Java_Websocket, a short video e-commerce Java Websocket solution developed from scratch

Introduction

Github: https://github.com/TooTallNate/Java-WebSocket

A barebones WebSocket server and client implementation written in 100% Java. Low-level classes are implemented java.niothat allow for a non-blocking event-driven model (similar to a web browser's WebSocket API ).

The implemented WebSocket protocol versions are:

Using Maven, add this dependency to your pom.xml:

<dependency>
  <groupId>org.java-websocket</groupId>
  <artifactId>Java-WebSocket</artifactId>
  <version>1.5.3</version>
</dependency>

Server example

Using Java-Websocket is very similar to using javascript websockets : you just take a client or server class and override its abstract methods by putting your application logic.

These methods are

  • onOpen
  • onMessage
  • onClose
  • onError
  • onStart (just for the server)
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;

import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;

public class SimpleServer extends WebSocketServer {
    
    

	public SimpleServer(InetSocketAddress address) {
    
    
		super(address);
	}

	@Override
	public void onOpen(WebSocket conn, ClientHandshake handshake) {
    
    
		conn.send("Welcome to the server!"); //This method sends a message to the new client
		broadcast( "new connection: " + handshake.getResourceDescriptor() ); //This method sends a message to all clients connected
		System.out.println("new connection to " + conn.getRemoteSocketAddress());
	}

	@Override
	public void onClose(WebSocket conn, int code, String reason, boolean remote) {
    
    
		System.out.println("closed " + conn.getRemoteSocketAddress() + " with exit code " + code + " additional info: " + reason);
	}

	@Override
	public void onMessage(WebSocket conn, String message) {
    
    
		System.out.println("received message from "	+ conn.getRemoteSocketAddress() + ": " + message);
	}

	@Override
	public void onMessage( WebSocket conn, ByteBuffer message ) {
    
    
        broadcast(message.array());
		System.out.println("received ByteBuffer from "	+ conn.getRemoteSocketAddress());
	}

	@Override
	public void onError(WebSocket conn, Exception ex) {
    
    
		System.err.println("an error occurred on connection " + conn.getRemoteSocketAddress()  + ":" + ex);
	}
	
	@Override
	public void onStart() {
    
    
        setConnectionLostTimeout(120)
		System.out.println("server started successfully");
	}


	public static void main(String[] args) {
    
    
		String host = "localhost";
		int port = 8887;
		WebSocketServer server = new SimpleServer(new InetSocketAddress(host, port));
		server.run();
	}
}

client example

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;

public class EmptyClient extends WebSocketClient {
    
    

	public EmptyClient(URI serverUri, Draft draft) {
    
    
		super(serverUri, draft);
	}

	public EmptyClient(URI serverURI) {
    
    
		super(serverURI);
	}

	@Override
	public void onOpen(ServerHandshake handshakedata) {
    
    
		send("Hello, it is me. Mario :)");
		System.out.println("new connection opened");
	}

	@Override
	public void onClose(int code, String reason, boolean remote) {
    
    
		System.out.println("closed with exit code " + code + " additional info: " + reason);
	}

	@Override
	public void onMessage(String message) {
    
    
		System.out.println("received message: " + message);
	}

	@Override
	public void onMessage(ByteBuffer message) {
    
    
		System.out.println("received ByteBuffer");
	}

	@Override
	public void onError(Exception ex) {
    
    
		System.err.println("an error occurred:" + ex);
	}

	public static void main(String[] args) throws URISyntaxException {
    
    		
		WebSocketClient client = new EmptyClient(new URI("ws://localhost:8887"));
		client.connect();
	}
}

Additional data attachment for the connection

Equivalent to extending properties/fields, attachment stores data directly on the WebSocket instance.

For example, you can use it to keep track of different clients or to store authentication information in an easy way.

code example

public class ServerAttachmentExample extends WebSocketServer {
    
    

  Integer index = 0;
  @Override
  public void onOpen(WebSocket conn, ClientHandshake handshake) {
    
    
    conn.setAttachment(index); //Set the attachment to the current index
    index++;
  }
     @Override
  public void onClose(WebSocket conn, int code, String reason, boolean remote) {
    
    
    // Get the attachment of this connection as Integer
    System.out.println(conn + " has left the room! ID: " + conn.<Integer>getAttachment());
  }   

principle

websocket.java
	private Object attachment;
	
    public <T> void setAttachment(T attachment) {
    
    
       this.attachment = attachment;
    }

Custom request header

    Map<String, String> httpHeaders = new HashMap<String, String>();
    httpHeaders.put("Cookie", "username=nemo");
    httpHeaders.put("Access-Control-Allow-Origin", "*");
    c = new ExampleClient(new URI("ws://localhost:8887"), httpHeaders);
    //Wer expect a successful connection
    c.connectBlocking();
    c.closeBlocking();

custom response header

Can solve cross-domain problems

public class ServerAdditionalHeaderExample extends WebSocketServer {
    
    
  @Override
  public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft,
      ClientHandshake request) throws InvalidDataException {
    
    
    ServerHandshakeBuilder builder = super
        .onWebsocketHandshakeReceivedAsServer(conn, draft, request);
    builder.put("Access-Control-Allow-Origin", "*");
    return builder;
  }

get response header

public void onOpen( WebSocket conn, ClientHandshake handshake ) {
    
    
	if (!handshake.hasFieldValue( "Cookie" )) {
    
    
		return;
	}
	String cookie = handshake.getFieldValue( "Cookie" );
}

Get request URL parameters

public class LakerServer extends WebSocketServer {
    
    
    @Override
    public void onOpen(WebSocket conn, ClientHandshake handshake) {
    
    
        //  如果请求为 ws://localhost:8887?roomid=1 
        //  则结果为  /?roomid=1 
       String queryString =  handshake.getResourceDescriptor();

reject handshake connection

Application scenario: For example, when some security checks are performed, if the specification is not met, the connection is refused.

public class ServerRejectHandshakeExample extends WebSocketServer {
    
    

  @Override
  public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft,
      ClientHandshake request) throws InvalidDataException {
    
    
    ServerHandshakeBuilder builder = super
        .onWebsocketHandshakeReceivedAsServer(conn, draft, request);
    //In this example we don't allow any resource descriptor ( "ws://localhost:8887/?roomid=1 will be rejected but ws://localhost:8887 is fine)
    if (!request.getResourceDescriptor().equals("/")) {
    
    
      throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, "Not accepted!");
    }
    //If there are no cookies set reject it as well.
    if (!request.hasFieldValue("Cookie")) {
    
    
      throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, "Not accepted!");
    }
    //If the cookie does not contain a specific value
    if (!request.getFieldValue("Cookie").equals("username=nemo")) {
    
    
      throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, "Not accepted!");
    }
    //If there is a Origin Field, it has to be localhost:8887
    if (request.hasFieldValue("Origin")) {
    
    
      if (!request.getFieldValue("Origin").equals("localhost:8887")) {
    
    
        throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, "Not accepted!");
      }
    }
    return builder;
  }

When thrown InvalidDataException, the server will send an HTTP code 404 to the client, resulting in a shutdown.

The principle is as follows

WebSocketImpl.java
                try {
    
    
                    response = wsl.onWebsocketHandshakeReceivedAsServer(this, d, handshake);
                  } catch (InvalidDataException e) {
    
    
                    log.trace("Closing due to wrong handshake. Possible handshake rejection", e);
                    // 如果收到的握手不正确,则关闭连接
                    closeConnectionDueToWrongHandshake(e);
                     -   write(generateHttpResponseDueToError(404));
                     -   flushAndClose(exception.getCloseCode(), exception.getMessage(), false);
                    return false;
                  } catch (RuntimeException e) {
    
    
                    log.error("Closing due to internal server error", e);
                    wsl.onWebsocketError(this, e);
                    // 如果 RuntimeException 出现服务器错误,则关闭连接
                    closeConnectionDueToInternalServerError(e);
                    -     write(generateHttpResponseDueToError(500));
                    -     flushAndClose(CloseFrame.NEVER_CONNECTED, exception.getMessage(), false);
                    return false;
                  }

Enable SO_REUSEADDR

SO_REUSEADDR is a socket option used to set whether to allow address reuse when binding a socket . It can quickly restart the server program when the server port is occupied.

In a TCP server program, when a connection is closed, the socket is kept in the operating system for a period of time (usually 2-4 minutes) to ensure that any delayed data arrives. During this time, if you start the server program again and try to use the same port number, an error that the address is already in use occurs. Setting the SO_REUSEADDR option solves this problem, allowing rebinding to the same address and port during this time .

It should be noted that if the SO_REUSEADDR option is set on multiple sockets at the same time, it may cause address conflicts between sockets. Therefore, care needs to be taken when using this option to ensure that only one socket is using a given address and port at any one time.

Remember that you cannot enable/disable SO_REUSEADDR when you have already started the connection.

ChatServer s = new ChatServer( port );
s.setReuseAddr( true );
s.start();

ExampleClient c = new ExampleClient( new URI( "ws://localhost:8887" ) );
c.setReuseAddr( true );
c.connect();

enableTCP_NODELAY

TCP_NODELAY is an option that can be set on a TCP socket to disable Nagle's algorithm. Nagle's algorithm is a property that improves network efficiency by reducing the number of small packets sent over the network.

When Nagle's algorithm is enabled, the TCP protocol stack waits for some data to accumulate before sending packets to reduce network overhead. However, this delay can adversely affect some real-time applications, such as online gaming and live video streaming.

By setting the TCP_NODELAY option, you can tell the TCP protocol stack to send data immediately, regardless of the size of the data packet. This reduces latency and improves performance for real-time applications, but may increase network load.

After enabling

  • Pros: Reduces latency and improves performance for real-time applications
  • Cons: May increase network load

Remember that you cannot enable/disable TCP_NODELAY when you have already initiated the connection. It only affects new connections.

ChatServer s = new ChatServer( port );
s.setTcpNoDelay( true );
s.start();

ExampleClient c = new ExampleClient( new URI( "ws://localhost:8887" ), new Draft_6455() );
c.setTcpNoDelay( true );
c.connect();

Multi-endpoint support Endpoint

There is no built-in support for multiple endpoints , so you'll have to implement it yourself.

  • Callbacks onOpenprovide access to handshake data.

  • handshake.getResourceDescriptor()You can parse the endpoint string from which the full URI will be obtained. Then you can call methods based on the endpoint string.

sample code

import org.java_websocket.*;
import org.java_websocket.server.*;
import org.java_websocket.client.*;
import org.java_websocket.handshake.*;
import java.util.*;
import java.net.*;

interface Endpoint {
    
    
	void onOpen(WebSocket socket);
	// add other event handlers here
}

public class EndpointServer extends WebSocketServer {
    
    
	private Map<String, Endpoint> endpoints = Collections.synchronizedMap(new HashMap<>());

	public static void main(String... args) {
    
    
		var server = new EndpointServer();
		server.endpoints.put("/greeting", socket -> socket.send("Hello!"));
		server.endpoints.put("/chat", socket -> socket.send("You have connected to chat"));
		server.start();

		var client = new Client("ws://localhost:" + server.getPort() + "/chat");
		client.connect();
	}

	public void onStart() {
    
    
		// ...
	}

	public void onOpen(WebSocket socket, ClientHandshake handshake) {
    
    
		String path = URI.create(handshake.getResourceDescriptor()).getPath();
		Endpoint endpoint = endpoints.get(path);
		if(endpoint != null)
			endpoint.onOpen(socket);
	}

	public void onMessage(WebSocket socket, String message) {
    
    
		// ...
	}

	public void onClose(WebSocket socket, int code, String message, boolean remote) {
    
    
		// ...
	}

	public void onError(WebSocket socket, Exception e) {
    
    
		e.printStackTrace();
	}
}


class Client extends WebSocketClient {
    
    
	public Client(String uri) {
    
    
		super(URI.create(uri));
	}
	public void onOpen(ServerHandshake handshake) {
    
    
	}

	public void onMessage(String message) {
    
    
		System.out.println(this + " received message: " + message);
	}

	public void onClose(int code, String message, boolean remote) {
    
    
	}

	public void onError(Exception e) {
    
    
		e.printStackTrace();
	}
}

Idle Check/Connection Lost Check

Idle Check or Connection Lost Check

Connection Loss Checking is a feature that detects if the connection to another endpoint is lost, for example due to a loss of wifi or mobile data signal.

To detect lost connections we use a heartbeat implementation.

Instrumentation runs at specified intervals (eg: 60 seconds) and performs the following actions on all connected endpoints:

  • Disconnects the endpoint if it has not sent a pong recently. Endpoints are given 1.5 times the time interval to reply to a PONG. Therefore, if the interval is 60 seconds, the endpoint has 90 seconds to respond.
  • Send a ping to the endpoint.

Detection is bidirectional, so a server can detect a lost client, and a client can detect a lost connection to the server.

Endpoints SHOULD respond to Ping frames with Pong frames as soon as practicable.

sample code

// 设置间隔为120秒
server.setConnectionLostTimeout(120);
// 间隔小于或等于 0 的值会导致检查被停用。
server.setConnectionLostTimeout( 0 );

Realization principle

// 实现原理 核心代码如下
long minimumPongTime;
synchronized (syncConnectionLost) {
    
    
   minimumPongTime = (long) (System.nanoTime() - (connectionLostTimeout * 1.5));
}
for (WebSocket webSocket : connections) {
    
    
   WebSocketImpl webSocketImpl = (WebSocketImpl) webSocket;
    if (webSocketImpl.getLastPong() < minimumPongTime) {
    
    
      // 如果最后一次收到PONG的时间差值 小于了 1.5倍的设置值
      // 则关闭连接
      log.trace("Closing connection due to no pong received: {}", webSocketImpl);
      webSocketImpl.closeConnection(CloseFrame.ABNORMAL_CLOSE,
    } else {
    
    
        // 为客户端发送Ping
      if (webSocketImpl.isOpen()) {
    
    
        webSocketImpl.sendPing();
      } else {
    
    
        log.trace("Trying to ping a non open connection: {}", webSocketImpl);
      }
    }
}

To verify , listen to Pong on the websocket server, here we set a check interval of 5 seconds.

@Override
public void onWebsocketPong(WebSocket conn, Framedata f) {
    
    
    logger.info("pong pong pong pong : {}", conn.getRemoteSocketAddress());
}
10:55:32.698 [WebSocketWorker-14] INFO  c.c.f.t.LakerTracker - pong pong pong pong : /127.0.0.1:54125
10:55:37.694 [WebSocketWorker-14] INFO  c.c.f.t.LakerTracker - pong pong pong pong : /127.0.0.1:54125
10:55:42.695 [WebSocketWorker-14] INFO  c.c.f.t.LakerTracker - pong pong pong pong : /127.0.0.1:54125
10:55:47.795 [WebSocketWorker-14] INFO  c.c.f.t.LakerTracker - pong pong pong pong : /127.0.0.1:54125

Internal thread Thread

Both websocket clients and servers use nio, although their architectures are completely different.

  • The client uses only one thread to perform read and write operations and event delivery
  • The server performs these operations in a multi-threaded manner:
    • The server has a selector thread selector and one or more worker threads workers .
    • The Selector thread performs all nio operations: it registers channels, selects keys for efficient reading and writing to channels.
    • The worker thread performs encoding/decoding or event delivery.
    • Selector thread selector thread and worker thread worker thread communicate through the queue . There are decoding queue, write queue and buffer queue.

The server-side loop decoding process is :

  • The selector thread takes an unused buffer from the " buffer queue ", puts the data of a read-ready channel into it, and puts it into the decoding queue.
  • The worker thread dequeues the websocket and consumes all allocated buffer contents.
  • It then puts the buffer back into the buffer queue so it can be reused.

Performance can be controlled by the number of worker threads , the number and size of buffers in the buffer queue, and the size of the channel's internal buffers.

WebSocketServerThe number of worker threads can be adjusted using a form of the constructor that takes parameters decodercount.

 public WebSocketServer(InetSocketAddress address) {
    
    
    this(address, AVAILABLE_PROCESSORS, null);
  }

  public WebSocketServer(InetSocketAddress address, int decodercount) {
    
    
    this(address, decodercount, null);
  }

WebSocketClient starts the following threads:

  • WebSocketTimer- Lost connection detection timer
  • WebSocketWriteThread-*- A thread that writes messages to another endpoint
  • WebSocketConnectReadThread-*- A thread that connects and reads messages from another endpoint

WebSocketServer starts the following threads:

  • WebSocketTimer- Lost connection detection timer
  • WebSocketWorker-*- Threads to decode incoming messages (number of threads depends on how many decoders you use)
  • WebSocketSelector-*- The thread on which the server selector runs (single-threaded)

Guess you like

Origin blog.csdn.net/abu935009066/article/details/130019729