B4J Library jServer v4.0 - Based on Jetty 11

Starting from B4J v9.80 this library is included as an internal library. Old library: https://www.b4x.com/android/forum/threads/141323/#content
This is a new version of jServer. It is based on Jetty 11.0.9. Previous versions were based on an early version of Jetty 9.
There were many improvements in Jetty during the last couple of years: https://github.com/eclipse/jetty.project/releases

It requires Java 11+.
It works with the standalone packager with these declarations:
B4X:
#PackagerProperty: AdditionalModuleInfoString = provides org.slf4j.spi.SLF4JServiceProvider with org.eclipse.jetty.logging.JettyLoggingServiceProvider;
#PackagerProperty: AdditionalModuleInfoString = provides org.eclipse.jetty.io.ssl.ALPNProcessor.Server with org.eclipse.jetty.alpn.java.server.JDK9ServerALPNProcessor;
#PackagerProperty: AdditionalModuleInfoString = provides org.eclipse.jetty.http.HttpFieldPreEncoder with org.eclipse.jetty.http2.hpack.HpackFieldPreEncoder, org.eclipse.jetty.http.Http1FieldPreEncoder;
#PackagerProperty: AdditionalModuleInfoString = uses org.eclipse.jetty.util.security.CredentialProvider;
#PackagerProperty: AdditionalModuleInfoString = uses org.eclipse.jetty.io.ssl.ALPNProcessor.Server;
#PackagerProperty: IncludedModules = jdk.charsets, jdk.crypto.ec
#CustomBuildAction: After Packager, %WINDIR%\System32\robocopy.exe, www temp\build\bin\www /E

Note that http sessions are not created automatically for WebSockets. You can use this filter handler to create it: https://www.b4x.com/android/forum/threads/safari-session-variables-in-b4j-v4.61652/post-389898 (apparently it relied on a feature not supported by WebSocket specs).

b4j_ws.js v0.93 is attached. It includes a fix suggested by @alwaysbusy: https://www.b4x.com/android/forum/threads/jserver-v4-0-based-on-jetty-11.140437/post-889892
 

Attachments

  • b4j_ws.zip
    1.3 KB · Views: 782
Last edited:

alwaysbusy

Expert
Licensed User
Longtime User
Initial tests with one of our biggest ABMaterial projects show no problems! 😊 Because it does use a small Java snippet for a CORS filter, I just had to make a minor change in the code:

Replaced:
B4X:
import javax.servlet.DispatcherType;
import javax.servlet.Filter;

With:
B4X:
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;

B4X:
' Cross-Origin Resource Sharing (CORS) Filter class
' Version 1.10
Sub Class_Globals
    Private cPath As String
    Private cSettings As Map
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize (Path As String, Settings As Map)
    cPath = Path
    cSettings = Settings
End Sub

'Return True to allow the request to proceed.
'Public Sub Filter (req As ServletRequest, resp As ServletResponse) As Boolean
'    Return True
'End Sub

Public Sub AddToServer (ServerObject As Server)
    Dim joServerWrapper As JavaObject = ServerObject
    Dim joMe As JavaObject = Me
    joMe.RunMethod("addFilter", Array As Object(joServerWrapper.GetField("context"), cPath, cSettings))
End Sub

#If JAVA
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map.Entry;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;

import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.FilterHolder;

import anywheresoftware.b4a.objects.collections.Map.MyMap;

public void addFilter(ServletContextHandler context, String path, MyMap settings) throws Exception {
    FilterHolder fh = new FilterHolder((Class<? extends Filter>) Class.forName("org.eclipse.jetty.servlets.CrossOriginFilter"));
    if (settings != null) {
        HashMap<String,String> m = new HashMap<String, String>();
        copyMyMap(settings, m, true); //integerNumbersOnly!
        fh.setInitParameters(m);
    }
    context.addFilter(fh, path, EnumSet.of(DispatcherType.REQUEST));
}

private void copyMyMap(MyMap m, java.util.Map<String, String> o, boolean integerNumbersOnly) {
    for (Entry<Object, Object> e : m.entrySet()) {
        String value;
        if (integerNumbersOnly && e.getValue() instanceof Number) {
            value = String.valueOf(((Number)e.getValue()).longValue());
        } else {
            value = String.valueOf(e.getValue());
            o.put(String.valueOf(e.getKey()), value);
        }
    }
}
#End If
 

alwaysbusy

Expert
Licensed User
Longtime User
Not sure if I should start a new topic, but as this is beta, I assume we can report stuff here.

I did find an issue where I want to set the MaxTextMessageSize on a WebSocket session.

I was using this snippet, but I get and error. From what I can find on the interweb, this should still work with Jetty 11.
B4X:
Dim MaxSize as int = 5*1024*1024
Dim jo As JavaObject = ws
jo = jo.GetFieldJO("session").RunMethod("getPolicy", Null)
jo.RunMethod("setMaxTextMessageSize", Array(MaxSize))

Error:
B4X:
java.lang.RuntimeException: Method: setMaxTextMessageSize not matched.
    at anywheresoftware.b4j.object.JavaObject.RunMethod(JavaObject.java:130)
    at b4j.example.pagecard0301._websocket_connected(pagecard0301.java:3256)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:111)
    at anywheresoftware.b4a.BA.raiseEvent(BA.java:98)
    at anywheresoftware.b4j.object.WebSocketModule$Adapter$ThreadHandler.run(WebSocketModule.java:203)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)

java.lang.RuntimeException: java.lang.RuntimeException: Method: setMaxTextMessageSize not matched.
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:140)
    at anywheresoftware.b4a.BA.raiseEvent(BA.java:98)
    at anywheresoftware.b4j.object.WebSocketModule$Adapter$ThreadHandler.run(WebSocketModule.java:203)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.RuntimeException: Method: setMaxTextMessageSize not matched.
    at anywheresoftware.b4j.object.JavaObject.RunMethod(JavaObject.java:130)
    at b4j.example.pagecard0301._websocket_connected(pagecard0301.java:3256)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:111)
    ... 7 more

EDIT: it appears the max size is no longer an Int, but must be a Long
B4X:
Dim MaxSize as Long = 5*1024*1024 ' <-----------
Dim jo As JavaObject = ws
jo = jo.GetFieldJO("session").RunMethod("getPolicy", Null)
jo.RunMethod("setMaxTextMessageSize", Array(MaxSize))

Alwaysbusy
 
Last edited:

alwaysbusy

Expert
Licensed User
Longtime User
@Erel Is there any chance the ClosedChannelException could also log a one-liner instead of the whole stack trace? This exception is always logged when someone e.g. changes pages or closes the Web App. Weirdly it only happens in Release mode, not in Debug mode. The logs are growing exponentially with this before the Websocket_disconnect event happens:

B4X:
java.nio.channels.ClosedChannelException
    at org.eclipse.jetty.websocket.core.internal.WebSocketSessionState.onEof(WebSocketSessionState.java:169)
    at org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession.onEof(WebSocketCoreSession.java:253)
    at org.eclipse.jetty.websocket.core.internal.WebSocketConnection.fillAndParse(WebSocketConnection.java:482)
    at org.eclipse.jetty.websocket.core.internal.WebSocketConnection.onFillable(WebSocketConnection.java:340)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:319)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)
    at org.eclipse.jetty.io.ssl.SslConnection$DecryptedEndPoint.onFillable(SslConnection.java:530)
    at org.eclipse.jetty.io.ssl.SslConnection.onFillable(SslConnection.java:379)
    at org.eclipse.jetty.io.ssl.SslConnection$2.succeeded(SslConnection.java:146)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)
    at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:412)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:381)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:268)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.lambda$new$0(AdaptiveExecutionStrategy.java:138)
    at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:407)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:894)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1038)
    at java.base/java.lang.Thread.run(Thread.java:834)

From what I read, Jetty 9 didn't do this.

I believe it would be in WebsocketModule.java, in the onWebSocketError method (untested).

B4X:
@Override
        public void onWebSocketError(Throwable cause) {
            super.onWebSocketError(cause);
            if (cause instanceof SocketTimeoutException || cause instanceof EOFException || cause instanceof ClosedChannelException) { // HERE
                System.err.println("onWebSocketError: " + cause.getMessage());
            } else {
                cause.printStackTrace();               
            }
        }

I'm running further tests with the new jServer. We do not see an immidiate speed gain for the moment, although these a big projects.

Alwaysbusy
 

alwaysbusy

Expert
Licensed User
Longtime User
I'm not sure there is anything you can do about this (it may be a jetty bug) but the very first time after the server is started and someone connects, it looks like the UpgradeRequest session is null. After that one time on the start, we don't see this again in the logs, even if coming from another computer and it does not interrupt the further working of the server app. The next time we restart the server, the same thing happens with the very first connection. I will investigate further.

B4X:
Private Sub WebSocket_Connected (WebSocket1 As WebSocket)    
    ws = WebSocket1
    Dim Session As HttpSession = ws.UpgradeRequest.GetSession
    Log("Session.IsInitialized: " & Session.IsInitialized) '<--- false
    
    If Session.IsNew Then  '<--- crash on IsNew (or getAttribute etc)
         Log(ABMShared.FormatDateTimeNow(Name) & "Session is new")
         session.Invalidate
         ...

Log:
B4X:
[main] INFO org.eclipse.jetty.server.AbstractConnector - Started ServerConnector@602e0143{HTTP/1.1, (http/1.1)}{0.0.0.0:55055}
[main] INFO org.eclipse.jetty.util.ssl.SslContextFactory - x509=X509@7fcbe147(ssl,h=[alain bailleul],a=[],w=[]) for Server@47db5fa5[provider=null,keyStore=file:///home/onetwo_dev/Work2020/keystore.jks,trustStore=null]
[main] INFO org.eclipse.jetty.server.AbstractConnector - Started ServerConnector@79517588{SSL, (ssl, alpn, h2, http/1.1)}{0.0.0.0:55056}
[main] INFO org.eclipse.jetty.server.Server - Started Server@1d730606{STARTING}[11.0.9,sto=0] @3241ms
tmpSession.IsInitialized: false
java.lang.RuntimeException: java.lang.RuntimeException: Object should first be initialized (HttpSession).
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:140)
    at anywheresoftware.b4a.BA.raiseEvent(BA.java:98)
    at anywheresoftware.b4j.object.WebSocketModule$Adapter$ThreadHandler.run(WebSocketModule.java:204)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.RuntimeException: Object should first be initialized (HttpSession).
    at anywheresoftware.b4a.AbsObjectWrapper.getObject(AbsObjectWrapper.java:49)
    at anywheresoftware.b4j.object.HttpSessionWrapper.getIsNew(HttpSessionWrapper.java:111)
    at b4j.example.pageregistration0101._websocket_connected(pageregistration0101.java:1382)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:111)
    ... 7 more

Alwaysbusy
 

alwaysbusy

Expert
Licensed User
Longtime User
There may be more to this. I tried the ChatRoom WebApp https://www.b4x.com/android/forum/t...ads-sessions-and-server-events.39969/#content, and it gives a similar error on the Session object when trying to login:

B4X:
Sub Enter_Click (Params As Map)
    Dim nf As Future = username.GetVal
    Dim name As String = nf.Value
    name = WebUtils.EscapeHtml(name.Trim)
    If ChatShared.AvoidDuplicates.ContainsKey(name.ToLowerCase) Then
        ws.Alert("Name already taken")
    Else If name.Length = 0 Then
        ws.Alert("Invalid name")
    Else
        ws.Session.SetAttribute("name", name) ' <--- crash on this line
        ChatShared.AvoidDuplicates.Put(name.ToLowerCase, name)
        WebUtils.RedirectTo(ws, "chat.html")
    End If
End Sub

B4X:
Waiting for debugger to connect...
Program started.
2022-05-11 13:10:10.371:INFO :oejs.Server:main: jetty-11.0.9; built: 2022-03-30T17:44:47.085Z; git: 243a48a658a183130a8c8de353178d154ca04f04; jvm 11.0.1+13
2022-05-11 13:10:10.524:INFO :oejss.DefaultSessionIdManager:main: Session workerName=node0
2022-05-11 13:10:10.560:INFO :oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@7205765b{/,file:///K:/SourceCode/Chatroom/Chatroom/Objects/www/,AVAILABLE}
2022-05-11 13:10:10.591:INFO :oejs.RequestLogWriter:main: Opened K:\SourceCode\Chatroom\Chatroom\Objects\logs\b4j-2022_05_11.request.log
2022-05-11 13:10:11.155:INFO :oejs.AbstractConnector:main: Started ServerConnector@731f8236{HTTP/1.1, (http/1.1)}{0.0.0.0:51042}
2022-05-11 13:10:11.185:INFO :oejs.Server:main: Started Server@56620197{STARTING}[11.0.9,sto=0] @1630ms
Emulated network latency: 100ms
Waiting for value (100 ms)
Error occurred on line: 26 (ChatLogin)
java.lang.RuntimeException: Object should first be initialized (HttpSession).
    at anywheresoftware.b4a.AbsObjectWrapper.getObject(AbsObjectWrapper.java:49)
    at anywheresoftware.b4j.object.HttpSessionWrapper.SetAttribute(HttpSessionWrapper.java:78)
    at b4j.example.chatlogin._enter_click(chatlogin.java:88)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at anywheresoftware.b4a.shell.Shell.runMethod(Shell.java:629)
    at anywheresoftware.b4a.shell.Shell.raiseEventImpl(Shell.java:237)
    at anywheresoftware.b4a.shell.Shell.raiseEvent(Shell.java:167)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:111)
    at anywheresoftware.b4a.shell.ShellBA.raiseEvent2(ShellBA.java:98)
    at anywheresoftware.b4a.BA.raiseEvent(BA.java:98)
    at anywheresoftware.b4j.object.WebSocketModule$Adapter$1.run(WebSocketModule.java:142)
    at anywheresoftware.b4a.keywords.SimpleMessageLoop.runMessageLoop(SimpleMessageLoop.java:47)
    at anywheresoftware.b4a.StandardBA.startMessageLoop(StandardBA.java:43)
    at anywheresoftware.b4a.shell.ShellBA.startMessageLoop(ShellBA.java:119)
    at anywheresoftware.b4a.keywords.Common.StartMessageLoop(Common.java:180)
    at anywheresoftware.b4a.shell.Shell.raiseEventImpl(Shell.java:309)
    at anywheresoftware.b4a.shell.Shell.raiseEvent(Shell.java:167)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:111)
    at anywheresoftware.b4a.shell.ShellBA.raiseEvent2(ShellBA.java:98)
    at anywheresoftware.b4a.BA.raiseEvent(BA.java:98)
    at b4j.example.main.main(main.java:29)

Alwaysbusy
 

Erel

B4X founder
Staff member
Licensed User
Longtime User
The last error is expected. It doesn't happen with the online example, right?

Http sessions are not created for WebSocket connections automatically. You need to add a filter that creates the http session.
It is done in the online example with:
1.
B4X:
srvr.AddFilter("/*", "SessionCreator", False)

2. SessionCreator filter class:
B4X:
Public Sub Filter(req As ServletRequest, resp As ServletResponse) As Boolean
    req.GetSession
    Return True
End Sub
 
Last edited:

Jmu5667

Well-Known Member
Licensed User
Longtime User
Will it be possible to have jServer 3.0 & jServer 4.0 libs available in the ide. We currently are using jSever 3.0 and do not have plans to move to Java 11+, at the moment.
 

alwaysbusy

Expert
Licensed User
Longtime User
The last error is expected. It doesn't happen with the online example, right?
It doesn't happen with jServer 3 either. We did not need to make use of that filter. Guess that is something new to take into consideration. I'll see if adding this filter has an influence on my first error.
 

OliverA

Expert
Licensed User
Longtime User
If only I could remember why that was not the solution for us as it created caching problems we did not want...
I'm halfway (yeah only 50%) sure it had to do with Safari back then.
 
Top