I'm working on an application that uses Websockets (Java EE 7) to send messages to all the connected clients asynchronously. The server (Websocket endpoint) should send these messages whenever a new article (an engagement modal in my app) is created.
Everytime a connection is established to the websocket endpoint, I'm adding the corresponding session to a list, which I could be able to access outside.
But the problem I had is, when I'm accessing this created websocket endpoint to which all the clients connected from outside (any other business class), I've get the existing instance (like a singleton).
So, can you please suggest me a way I can get an existing instance of the websocket endpoint, as I can't create it as new MyWebsocketEndPoint() coz it'll be created by the websocket internal mechanism whenever the request from a client is received.
For a ref:
private static WebSocketEndPoint INSTANCE = null;
public static WebSocketEndPoint getInstance() {
if(INSTANCE == null) {
// Instead of creating a new instance, I need an existing one
INSTANCE = new WebSocketEndPoint ();
}
return INSTANCE;
}
Thanks in advance.
The container creates a separate instance of the endpoint for every client connection, so you can't do what you're trying to do. But I think what you're trying to do is send a message to all the active client connections when an event occurs, which is fairly straightforward.
The javax.websocket.Session class has the getBasicRemote method to retrieve a RemoteEndpoint.Basic instance that represents the endpoint associated with that session.
You can retrieve all the open sessions by calling Session.getOpenSessions(), then iterate through them. The loop will send each client connection a message. Here's a simple example:
#ServerEndpoint("/myendpoint")
public class MyEndpoint {
#OnMessage
public void onMessage(Session session, String message) {
try {
for (Session s : session.getOpenSessions()) {
if (s.isOpen()) {
s.getBasicRemote().sendText(message);
}
} catch (IOException ex) { ... }
}
}
But in your case, you probably want to use CDI events to trigger the update to all the clients. In that case, you'd create a CDI event that a method in your Websocket endpoint class observes:
#ServerEndpoint("/myendpoint")
public class MyEndpoint {
// EJB that fires an event when a new article appears
#EJB
ArticleBean articleBean;
// a collection containing all the sessions
private static final Set<Session> sessions =
Collections.synchronizedSet(new HashSet<Session>());
#OnOpen
public void onOpen(final Session session) {
// add the new session to the set
sessions.add(session);
...
}
#OnClose
public void onClose(final Session session) {
// remove the session from the set
sessions.remove(session);
}
public void broadcastArticle(#Observes #NewArticleEvent ArticleEvent articleEvent) {
synchronized(sessions) {
for (Session s : sessions) {
if (s.isOpen()) {
try {
// send the article summary to all the connected clients
s.getBasicRemote().sendText("New article up:" + articleEvent.getArticle().getSummary());
} catch (IOException ex) { ... }
}
}
}
}
}
The EJB in the above example would do something like:
...
#Inject
Event<ArticleEvent> newArticleEvent;
public void publishArticle(Article article) {
...
newArticleEvent.fire(new ArticleEvent(article));
...
}
See the Java EE 7 Tutorial chapters on WebSockets and CDI Events.
Edit: Modified the #Observer method to use an event as a parameter.
Edit 2: wrapped the loop in broadcastArticle in synchronized, per #gcvt.
Edit 3: Updated links to Java EE 7 Tutorial. Nice job, Oracle. Sheesh.
Actually, WebSocket API provides a way how you can control endpoint instantiation. See https://tyrus.java.net/apidocs/1.2.1/javax/websocket/server/ServerEndpointConfig.Configurator.html
simple sample (taken from Tyrus - WebSocket RI test):
public static class MyServerConfigurator extends ServerEndpointConfig.Configurator {
public static final MyEndpointAnnotated testEndpoint1 = new MyEndpointAnnotated();
public static final MyEndpointProgrammatic testEndpoint2 = new MyEndpointProgrammatic();
#Override
public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
if (endpointClass.equals(MyEndpointAnnotated.class)) {
return (T) testEndpoint1;
} else if (endpointClass.equals(MyEndpointProgrammatic.class)) {
return (T) testEndpoint2;
}
throw new InstantiationException();
}
}
You need to register this to an endpoint:
#ServerEndpoint(value = "/echoAnnotated", configurator = MyServerConfigurator.class)
public static class MyEndpointAnnotated {
#OnMessage
public String onMessage(String message) {
assertEquals(MyServerConfigurator.testEndpoint1, this);
return message;
}
}
or you can use it with programmatic endpoints as well:
public static class MyApplication implements ServerApplicationConfig {
#Override
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> endpointClasses) {
return new HashSet<ServerEndpointConfig>
(Arrays.asList(ServerEndpointConfig.Builder
.create(MyEndpointProgrammatic.class, "/echoProgrammatic")
.configurator(new MyServerConfigurator())
.build()));
}
#Override
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
return new HashSet<Class<?>>(Arrays.asList(MyEndpointAnnotated.class));
}
Of course it is up to you if you will have one configurator used for all endpoints (ugly ifs as in presented snippet) or if you'll create separate configurator for each endpoint.
Please do not copy presented code as it is - this is only part of Tyrus tests and it does violate some of the basic OOM paradigms.
See https://github.com/tyrus-project/tyrus/blob/1.2.1/tests/e2e/src/test/java/org/glassfish/tyrus/test/e2e/GetEndpointInstanceTest.java for complete test.
Related
I'm using the JMS (Java Messaging System) to test a standard application between a Client and a Server.
The application is divided by layers, in the client module, I have 3 different files, there is the Main which is like the application itself, the file of the controller that has a ListView with the list of messages and finally the third file that basically is the one that is responsible for sending and receiving incoming messages.
As it is divided into layers, when I start the application, the Controller starts an instance of the Messaging class, so that it can send the messages that are written in the interface and of course store them in the ListView that I have with all message history.
The question is the following, when I send a message, it passes it from the controller to the class of the messaging so it could be sent to the server, but when I do the reverse process, that the messaging receives a new message, I can not find the way to pass it back to the controller, since the controller was the one who initiated the messaging instance and not the other way around.
THE CONTROLLER LOOKS THIS WAY:
public class LoanClientController implements Initializable{
public ApplicationGateway applicationGateway = new ApplicationGateway(){};
public TextField tfSsn;
public TextField tfAmount;
public TextField tfTime;
public ListView<ListViewLine> lvLoanRequestReply;
#Override
public void initialize(URL location, ResourceBundle resources) {
tfSsn.setText("123456");
tfAmount.setText("80000");
tfTime.setText("30");
}
#FXML
public void btnSendLoanRequestClicked() throws Exception {
int ssn = Integer.parseInt(tfSsn.getText());
int amount = Integer.parseInt(tfAmount.getText());
int time = Integer.parseInt(tfTime.getText());
LoanRequest loanRequest = new LoanRequest(ssn,amount,time);
ListViewLine listViewLine = applicationGateway.sendMessage(loanRequest);
lvLoanRequestReply.getItems().add(listViewLine);
applicationGateway.setLvLoanRequestReply(lvLoanRequestReply);
lvLoanRequestReply.refresh();
}
}
ON THE OTHER SIDE, THE APPLICATION GATEWAY a.k.a. "MESSAGING CLASS" is the following:
public class ApplicationGateway implements MessageListener {
public MessagingGateway messagingGateway = new MessagingGateway("Client-Broker", "Broker-Client");
public ApplicationGateway(){
try {
messagingGateway.getConsumer().setMessageListener(this);
} catch (JMSException e) {
e.printStackTrace();
}
}
#Override
public void onMessage(Message message) {
try {
Gson gson = new Gson();
LoanReply loanReply = gson.fromJson(((TextMessage) message).getText(), LoanReply.class);
} catch (JMSException e) {
e.printStackTrace();
}
}
}
Basically, what I'm trying to do is when a new message arrives via the onMessage method, send the value to the Controller.
I believe this question is not a duplicate of Server sent event with Jersey: EventOutput is not closed after client drops, but probably related to Jersey Server-Sent Events - write to broken connection does not throw exception.
In chapter 15.4.2 of the Jersey documentation, the SseBroadcaster is described:
However, the SseBroadcaster internally identifies and handles also client disconnects. When a client closes the connection the broadcaster detects this and removes the stale connection from the internal collection of the registered EventOutputs as well as it frees all the server-side resources associated with the stale connection.
I cannot confirm this. In the following testcase, I see the subclassed SseBroadcaster's onClose() method never being called: not when the EventInput is closed, and not when another message is broadcasted.
public class NotificationsResourceTest extends JerseyTest {
final static Logger log = LoggerFactory.getLogger(NotificationsResourceTest.class);
final static CountingSseBroadcaster broadcaster = new CountingSseBroadcaster();
public static class CountingSseBroadcaster extends SseBroadcaster {
final AtomicInteger connectionCounter = new AtomicInteger(0);
public EventOutput createAndAttachEventOutput() {
EventOutput output = new EventOutput();
if (add(output)) {
int cons = connectionCounter.incrementAndGet();
log.debug("Active connection count: "+ cons);
}
return output;
}
#Override
public void onClose(final ChunkedOutput<OutboundEvent> output) {
int cons = connectionCounter.decrementAndGet();
log.debug("A connection has been closed. Active connection count: "+ cons);
}
#Override
public void onException(final ChunkedOutput<OutboundEvent> chunkedOutput, final Exception exception) {
log.trace("An exception has been detected", exception);
}
public int getConnectionCount() {
return connectionCounter.get();
}
}
#Path("notifications")
public static class NotificationsResource {
#GET
#Produces(SseFeature.SERVER_SENT_EVENTS)
public EventOutput subscribe() {
log.debug("New stream subscription");
EventOutput eventOutput = broadcaster.createAndAttachEventOutput();
return eventOutput;
}
}
#Override
protected Application configure() {
ResourceConfig config = new ResourceConfig(NotificationsResource.class);
config.register(SseFeature.class);
return config;
}
#Test
public void test() throws Exception {
// check that there are no connections
assertEquals(0, broadcaster.getConnectionCount());
// connect subscriber
log.info("Connecting subscriber");
EventInput eventInput = target("notifications").request().get(EventInput.class);
assertFalse(eventInput.isClosed());
// now there are connections
assertEquals(1, broadcaster.getConnectionCount());
// push data
log.info("Broadcasting data");
String payload = UUID.randomUUID().toString();
OutboundEvent chunk = new OutboundEvent.Builder()
.mediaType(MediaType.TEXT_PLAIN_TYPE)
.name("message")
.data(payload)
.build();
broadcaster.broadcast(chunk);
// read data
log.info("Reading data");
InboundEvent inboundEvent = eventInput.read();
assertNotNull(inboundEvent);
assertEquals(payload, inboundEvent.readData());
// close subscription
log.info("Closing subscription");
eventInput.close();
assertTrue(eventInput.isClosed());
// at this point, the subscriber has disconnected itself,
// but jersey doesnt realise that
assertEquals(1, broadcaster.getConnectionCount());
// wait, give TCP a chance to close the connection
log.debug("Sleeping for some time");
Thread.sleep(10000);
// push data again, this should really flush out the not-connected client
log.info("Broadcasting data again");
broadcaster.broadcast(chunk);
Thread.sleep(100);
// there is no subscriber anymore
assertEquals(0, broadcaster.getConnectionCount()); // FAILS!
}
}
Maybe JerseyTest is not a good way to test this. In a less ... clinical setup, where a JavaScript EventSource is used, I see onClose() being called, but only after a message is broadcasted on the previously closed connection.
What am I doing wrong?
Why doesn't SseBroadcaster detect the closing of the connection by the client?
Follow-up
I've found JERSEY-2833 which was rejected with Works as designed:
According to the Jersey Documentation in SSE chapter (https://jersey.java.net/documentation/latest/sse.html) in 15.4.1 it's mentioned that Jersey does not explicitly close the connection, it's the responsibility of the resource method or the client.
What does that mean exactly? Should the resource enforce a timeout and kill all active and closed-by-client connections?
In the documentation of the constructor org.glassfish.jersey.media.sse.SseBroadcaster.SseBroadcaster(), it says:
Creates a new instance. If this constructor is called by a subclass, it assumes the the reason for the subclass to exist is to implement onClose(org.glassfish.jersey.server.ChunkedOutput) and onException(org.glassfish.jersey.server.ChunkedOutput, Exception)methods, so it adds the newly created instance as the listener. To avoid this, subclasses may call SseBroadcaster(Class) passing their class as an argument.
So you should not leave default constructor and try implementing your constructor invoking super with your class:
public CountingSseBroadcaster(){
super(CountingSseBroadcaster.class);
}
I believe it might be better to set a timeout on your resource and kill only that connection, for example:
#Path("notifications")
public static class NotificationsResource {
#GET
#Produces(SseFeature.SERVER_SENT_EVENTS)
public EventOutput subscribe() {
log.debug("New stream subscription");
EventOutput eventOutput = broadcaster.createAndAttachEventOutput();
new Timer().schedule( new TimerTask()
{
#Override public void run()
{
eventOutput.close()
}
}, 10000); // 10 second timeout
return eventOutput;
}
}
Im wondering if by subclassing you may have changed the behaviour.
#Override
public void onClose(final ChunkedOutput<OutboundEvent> output) {
int cons = connectionCounter.decrementAndGet();
log.debug("A connection has been closed. Active connection count: "+ cons);
}
In this you don't close the ChunkedOutput so it won't release the connection. Could this be the problem?
Is it possible to have my app update the config settings at runtime? I can easily expose the settings I want in my UI but is there a way to allow the user to update settings and make them permanent ie save them to the config.yaml file? The only way I can see it to update the file by hand then restart the server which seems a bit limiting.
Yes. It is possible to reload the service classes at runtime.
Dropwizard by itself does not have the way to reload the app, but jersey has.
Jersey uses a container object internally to maintain the running application. Dropwizard uses the ServletContainer class of Jersey to run the application.
How to reload the app without restarting it -
Get a handle to the container used internally by jersey
You can do this by registering a AbstractContainerLifeCycleListener in Dropwizard Environment before starting the app. and implement its onStartup method as below -
In your main method where you start the app -
//getting the container instance
environment.jersey().register(new AbstractContainerLifecycleListener() {
#Override
public void onStartup(Container container) {
//initializing container - which will be used to reload the app
_container = container;
}
});
Add a method to your app to reload the app. It will take in the list of string which are the names of the service classes you want to reload. This method will call the reload method of the container with the new custom DropWizardConfiguration instance.
In your Application class
public static synchronized void reloadApp(List<String> reloadClasses) {
DropwizardResourceConfig dropwizardResourceConfig = new DropwizardResourceConfig();
for (String className : reloadClasses) {
try {
Class<?> serviceClass = Class.forName(className);
dropwizardResourceConfig.registerClasses(serviceClass);
System.out.printf(" + loaded class %s.\n", className);
} catch (ClassNotFoundException ex) {
System.out.printf(" ! class %s not found.\n", className);
}
}
_container.reload(dropwizardResourceConfig);
}
For more details see the example documentation of jersey - jersey example for reload
Consider going through the code and documentation of following files in Dropwizard/Jersey for a better understanding -
Container.java
ContainerLifeCycleListener.java
ServletContainer.java
AbstractContainerLifeCycleListener.java
DropWizardResourceConfig.java
ResourceConfig.java
No.
Yaml file is parsed at startup and given to the application as Configuration object once and for all. I believe you can change the file after that but it wouldn't affect your application until you restart it.
Possible follow up question: Can one restart the service programmatically?
AFAIK, no. I've researched and read the code somewhat for that but couldn't find a way to do that yet. If there is, I'd love to hear that :).
I made a task that reloads the main yaml file (it would be useful if something in the file changes). However, it is not reloading the environment. After researching this, Dropwizard uses a lot of final variables and it's quite hard to reload these on the go, without restarting the app.
class ReloadYAMLTask extends Task {
private String yamlFileName;
ReloadYAMLTask(String yamlFileName) {
super("reloadYaml");
this.yamlFileName = yamlFileName;
}
#Override
public void execute(ImmutableMultimap<String, String> parameters, PrintWriter output) throws Exception {
if (yamlFileName != null) {
ConfigurationFactoryFactory configurationFactoryFactory = new DefaultConfigurationFactoryFactory<ReportingServiceConfiguration>();
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
ObjectMapper objectMapper = Jackson.newObjectMapper();
final ConfigurationFactory<ServiceConfiguration> configurationFactory = configurationFactoryFactory.create(ServiceConfiguration.class, validator, objectMapper, "dw");
File confFile = new File(yamlFileName);
configurationFactory.build(new File(confFile.toURI()));
}
}
}
You can change the configuration in the YAML and read it while your application is running. This will not however restart the server or change any server configurations. You will be able to read any changed custom configurations and use them. For example, you can change the logging level at runtime or reload other custom settings.
My solution -
Define a custom server command. You should use this command to start your application instead of the "server" command.
ArgsServerCommand.java
public class ArgsServerCommand<WC extends WebConfiguration> extends EnvironmentCommand<WC> {
private static final Logger LOGGER = LoggerFactory.getLogger(ArgsServerCommand.class);
private final Class<WC> configurationClass;
private Namespace _namespace;
public static String COMMAND_NAME = "args-server";
public ArgsServerCommand(Application<WC> application) {
super(application, "args-server", "Runs the Dropwizard application as an HTTP server specific to my settings");
this.configurationClass = application.getConfigurationClass();
}
/*
* Since we don't subclass ServerCommand, we need a concrete reference to the configuration
* class.
*/
#Override
protected Class<WC> getConfigurationClass() {
return configurationClass;
}
public Namespace getNamespace() {
return _namespace;
}
#Override
protected void run(Environment environment, Namespace namespace, WC configuration) throws Exception {
_namespace = namespace;
final Server server = configuration.getServerFactory().build(environment);
try {
server.addLifeCycleListener(new LifeCycleListener());
cleanupAsynchronously();
server.start();
} catch (Exception e) {
LOGGER.error("Unable to start server, shutting down", e);
server.stop();
cleanup();
throw e;
}
}
private class LifeCycleListener extends AbstractLifeCycle.AbstractLifeCycleListener {
#Override
public void lifeCycleStopped(LifeCycle event) {
cleanup();
}
}
}
Method to reload in your Application -
_ymlFilePath = null; //class variable
public static boolean reloadConfiguration() throws IOException, ConfigurationException {
boolean reloaded = false;
if (_ymlFilePath == null) {
List<Command> commands = _configurationBootstrap.getCommands();
for (Command command : commands) {
String commandName = command.getName();
if (commandName.equals(ArgsServerCommand.COMMAND_NAME)) {
Namespace namespace = ((ArgsServerCommand) command).getNamespace();
if (namespace != null) {
_ymlFilePath = namespace.getString("file");
}
}
}
}
ConfigurationFactoryFactory configurationFactoryFactory = _configurationBootstrap.getConfigurationFactoryFactory();
ValidatorFactory validatorFactory = _configurationBootstrap.getValidatorFactory();
Validator validator = validatorFactory.getValidator();
ObjectMapper objectMapper = _configurationBootstrap.getObjectMapper();
ConfigurationSourceProvider provider = _configurationBootstrap.getConfigurationSourceProvider();
final ConfigurationFactory<CustomWebConfiguration> configurationFactory = configurationFactoryFactory.create(CustomWebConfiguration.class, validator, objectMapper, "dw");
if (_ymlFilePath != null) {
// Refresh logging level.
CustomWebConfiguration webConfiguration = configurationFactory.build(provider, _ymlFilePath);
LoggingFactory loggingFactory = webConfiguration.getLoggingFactory();
loggingFactory.configure(_configurationBootstrap.getMetricRegistry(), _configurationBootstrap.getApplication().getName());
// Get my defined custom settings
CustomSettings customSettings = webConfiguration.getCustomSettings();
reloaded = true;
}
return reloaded;
}
Although this feature isn't supported out of the box by dropwizard, you're able to accomplish this fairly easy with the tools they give you.
Before I get started, note that this isn't a complete solution for the question asked as it doesn't persist the updated config values to the config.yml. However, this would be easy enough to implement yourself simply by writing to the config file from the application. If anyone would like to write this implementation feel free to open a PR on the example project I've linked below.
Code
Start off with a minimal config:
config.yml
myConfigValue: "hello"
And it's corresponding configuration file:
ExampleConfiguration.java
public class ExampleConfiguration extends Configuration {
private String myConfigValue;
public String getMyConfigValue() {
return myConfigValue;
}
public void setMyConfigValue(String value) {
myConfigValue = value;
}
}
Then create a task which updates the config:
UpdateConfigTask.java
public class UpdateConfigTask extends Task {
ExampleConfiguration config;
public UpdateConfigTask(ExampleConfiguration config) {
super("updateconfig");
this.config = config;
}
#Override
public void execute(Map<String, List<String>> parameters, PrintWriter output) {
config.setMyConfigValue("goodbye");
}
}
Also for demonstration purposes, create a resource which allows you to get the config value:
ConfigResource.java
#Path("/config")
public class ConfigResource {
private final ExampleConfiguration config;
public ConfigResource(ExampleConfiguration config) {
this.config = config;
}
#GET
public Response handleGet() {
return Response.ok().entity(config.getMyConfigValue()).build();
}
}
Finally wire everything up in your application:
ExampleApplication.java (exerpt)
environment.jersey().register(new ConfigResource(configuration));
environment.admin().addTask(new UpdateConfigTask(configuration));
Usage
Start up the application then run:
$ curl 'http://localhost:8080/config'
hello
$ curl -X POST 'http://localhost:8081/tasks/updateconfig'
$ curl 'http://localhost:8080/config'
goodbye
How it works
This works simply by passing the same reference to the constructor of ConfigResource.java and UpdateConfigTask.java. If you aren't familiar with the concept see here:
Is Java "pass-by-reference" or "pass-by-value"?
The linked classes above are to a project I've created which demonstrates this as a complete solution. Here's a link to the project:
scottg489/dropwizard-runtime-config-example
Footnote: I haven't verified this works with the built in configuration. However, the dropwizard Configuration class which you need to extend for your own configuration does have various "setters" for internal configuration, but it may not be safe to update those outside of run().
Disclaimer: The project I've linked here was created by me.
I have to implement RMI with a JSF application, but I haven't found any tutorials on how to integrate these two technologies. I have very little experience with Java, only one simple app developed with JSF so I'm still a beginner.
If you have knowledge of any links that might be useful please share, or tips if you can explain the main points I have to go over in order to structure/configure my project correctly.
Thank you in advance.
Update:
This is the project's requirements:
"Consider a distributed web application called “job-searching service”. The application should keep record of all job offers in the past year mentioning whether the job is already taken or not. A user should be able to post a job offer and search through the job offers either by a date or by categories.
When implementing this system using Distributed Objects technologies consider the following constraints:
A Remote Object should expose the “post jobs” system functionality, letting the user to add a job to the system.
A Remote Object should expose the “search jobs” functionality, which allows the user to search for jobs either by a date or by a category.
All job offers must have a job title, company name, deadline, contact details and job specification.
Your task:
Design, implement and test the distributed application using Java or .NET specific technologies. (RMI or .NET Remoting technologies are compulsory)"
There's no mystery on doing this. Remember that JSF managed beans are just Java classes, so the only thing here will be your application design. For doing this, you can have a (very) basic skeleton like this:
In the JSF Project:
#ManagedBean
#RequestScoped
public class LoginBean {
private String user;
private String password;
public LoginBean() {
}
//getters and setters...
public String validateUser() {
UserService userService = new UserService();
if (userService.validateUser(user, password)) {
return "success";
}
return "problems";
}
}
public class UserService extends {
public UserService() {
}
public boolean validateUser(String user, String password) {
//do the Java RMI client job here...
boolean result = false;
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try {
String name = "UserServiceRMI";
Registry registry = LocateRegistry.getRegistry(args[0]);
UserServiceRMI userServiceRMI = (UserServiceRMI) registry.lookup(name);
result = userServiceRMI.validateUser(user, password);
} catch (Exception e) {
//you can (and must) do a better error handling
System.out.println("UserService exception:");
e.printStackTrace();
}
return result;
}
}
In the RMI Server project:
public interface UserRMIService extends java.rmi.Remote {
boolean validateUser(String user, String password) throws java.rmi.RemoteException;
}
public class UserService implements UserRMIService {
public UserService() {
super();
}
public boolean validateUser(String user, String password) {
//do the user validation here...
//check against the database or another way you want/need
}
public static void main(String[] args) {
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try {
String name = "UserRMIService";
UserRMIService engine = new UserService();
UserRMIService stub =
(UserRMIService) UnicastRemoteObject.exportObject(engine, 0);
Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);
System.out.println("UserService bound");
} catch (Exception e) {
//you can (and must) do a better error handling
System.out.println("UserService exception:");
e.printStackTrace();
}
}
}
When you design an a client that is going to connect to a lot of servers, like a crawler.
You will code something like that :
// the pipeline
public class CrawlerPipelineFactory implements ChannelPipelineFactory {
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(new CrawlerHandler());
}
}
// the channel handler
public class CrawlerHandler extends SimpleChannelHandler {
#Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
// ...
}
}
// the main :
public static void main(){
ChannelFactory factory = new NioClientSocketChannelFactory(Executors.newCachedThreadPool(),Executors.newCachedThreadPool());
ClientBootstrap scannerBootstrap = new ClientBootstrap(factory);
scannerBootstrap.setPipelineFactory(new CrawlerPipelineFactory());
while(true){
MyURL url = stack.pop();
ChannelFuture connect = scannerBootstrap.connect(url.getSocketAddress());
}
}
Now when you are in your ApplicationHandler, the stuff that implements your SimpleChannelHandler or WhatEverStreamHandler, (CrawlerHander in the example) the only piece of information you get is the socketAdress you are connecting to that you can recover in "public void channelConnected()" function.
Ok but what if I want to recover some user data, like the MyURL object you see in my code example ?
I use a dirty hack, I use a Map<"ip:port",MyURL> so I can retrieve the associated data in channelConnected because I know ip:port i'm connected on.
This hack is really dirty, it won't work if you are connecting simultaneously to the same server (or you'll have to bind to a local port and use a key like "localport:ip:remoteport" but it's so dirty).
So I'm seeking what is the good way to pass data the the CrawlerHander ?
It would be cool if we could pass this data via the connect() method of the bootstrap. I know I can pass argument in my ChannelPipelineFactory.getPipeline() because it's invoked via connect(). But now we can't, so here is another dirty hack I use :
EDIT:
// the main
while(!targets.isEmpty()){
client.connect("localhost",111); // we will never connect to localhost, it's a hack
}
// the pipleline
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(
new CrawlerHandler(targets.pop()) // I specify each new host to connect here
);
}
// in my channel handler
// Now I have the data I want in the constructor, so I m sure I get them before everything is called
public class CrawlerHandler extends SimpleChannelHandler {
ExtraParameter target;
public CrawlerHandler(ExtraParameter target) {
this.target = target;
// but, and it's the most dirty part, I have to abort the connection to localhost, and reinit a new connection to the real target
boolean bFirstConnect=true;
#Override
public void connectRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
if(bFirstConnect){
bFirstConnect = false;
ctx.getChannel().connect(target.getSocketAddr());
}
You can pass variables to Channel via Bootstrap.
Netty.io 4.1 & SO - Adding an attribute to a Channel before creation
Update to this answer while very late.
You can pass the data to the newly connected channel/channel handler using ChannelLocal or in ChannelHandlerContext (or in the Channel it self in latest Netty 3.x) using a connect future listener. In below example, ChannelLocal is used.
public class ChannelDataHolder {
public final static ChannelLocal<String> CHANNEL_URL = new ChannelLocal<String>(true);
}
// for each url in bootstrap
MyURL url = ....;
ChannelFuture cf = scannerBootstrap.connect(url.getSocketAddress());
final String urlString = url.getUrl();
cf.addListener(new ChannelFutureListener() {
#Override
public void operationComplete(ChannelFuture future) throws Exception {
ChannelDataHolder.CHANNEL_URL.set(future.getChannel(), urlString);
}
});
//In the handler
public class CrawlerHandler extends SimpleChannelHandler {
#Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
String urlString = ChannelDataHolder.CHANNEL_URL.get(ctx.getChannel());
// ...use the data here
}
}
Note: instead of ChannelLocal, you can set and get the data using
ChannelHandlerContext.setAttachment()/getAttachment()
Channel.setAttachment()/getAttachment() in latest 3.x version of Netty
but both approaches does not support type safety.