tcp-server 1.0.1-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +123 -0
- data/Rakefile +56 -0
- data/exe/tcp_server +17 -0
- data/lib/client.rb +578 -0
- data/lib/log.rb +207 -0
- data/lib/server/argument_parser.rb +106 -0
- data/lib/server/channel_initializer.rb +124 -0
- data/lib/server/config.rb +30 -0
- data/lib/server/instance_methods.rb +91 -0
- data/lib/server/listenable.rb +41 -0
- data/lib/server/message_handler.rb +56 -0
- data/lib/server/modular_handler.rb +119 -0
- data/lib/server/server.rb +51 -0
- data/lib/server/shutdown_hook.rb +35 -0
- data/lib/server/version.rb +16 -0
- data/lib/server.rb +59 -0
- metadata +148 -0
data/lib/log.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
|
4
|
+
# =begin
|
5
|
+
|
6
|
+
# Copyright Nels Nelson 2016-2019 but freely usable (see license)
|
7
|
+
|
8
|
+
# =end
|
9
|
+
|
10
|
+
require 'java'
|
11
|
+
require 'logger'
|
12
|
+
|
13
|
+
require 'log4j-2'
|
14
|
+
|
15
|
+
# The LogInitialization module
|
16
|
+
module LogInitialization
|
17
|
+
LibDirPath = File.expand_path(__dir__) unless defined?(LibDirPath)
|
18
|
+
ProjectDirPath = File.expand_path(File.dirname(LibDirPath)) unless defined?(ProjectDirPath)
|
19
|
+
LogsDirPath = File.expand_path(File.join(ProjectDirPath, 'logs'))
|
20
|
+
Dir.mkdir(LogsDirPath) unless File.exist?(LogsDirPath)
|
21
|
+
ServerLogFile = File.join(LogsDirPath, 'server.log')
|
22
|
+
RollLogFileNameTemplate = 'server-%d{yyyy-MM-dd}.log.gz'.freeze
|
23
|
+
RollingLogFilePath = File.join(LogsDirPath, RollLogFileNameTemplate)
|
24
|
+
File.write(ServerLogFile, '') unless File.file? ServerLogFile
|
25
|
+
LoggerPatternTemplate = '%d{ABSOLUTE} %-5p [%c{1}] %m%n'.freeze
|
26
|
+
|
27
|
+
# rubocop: disable Metrics/AbcSize
|
28
|
+
# rubocop: disable Metrics/MethodLength
|
29
|
+
def self.init_log4j(log_level = org.apache.logging.log4j.Level::INFO)
|
30
|
+
java.lang::System.setProperty('log4j.shutdownHookEnabled', java.lang::Boolean.toString(false))
|
31
|
+
factory = org.apache.logging.log4j.core.config.builder.api::ConfigurationBuilderFactory
|
32
|
+
config = factory.newConfigurationBuilder()
|
33
|
+
|
34
|
+
log_level = org.apache.logging.log4j.Level.to_level(log_level.to_s.upcase) if log_level.is_a?(Symbol)
|
35
|
+
config.setStatusLevel(log_level)
|
36
|
+
config.setConfigurationName('websocket')
|
37
|
+
|
38
|
+
# create a console appender
|
39
|
+
target = org.apache.logging.log4j.core.appender::ConsoleAppender::Target::SYSTEM_OUT
|
40
|
+
layout = config.newLayout('PatternLayout')
|
41
|
+
layout = layout.addAttribute('pattern', LoggerPatternTemplate)
|
42
|
+
appender = config.newAppender('stdout', 'CONSOLE')
|
43
|
+
appender = appender.addAttribute('target', target)
|
44
|
+
appender = appender.add(layout)
|
45
|
+
config.add(appender)
|
46
|
+
|
47
|
+
# create a root logger
|
48
|
+
root_logger = config.newRootLogger(log_level)
|
49
|
+
root_logger = root_logger.add(config.newAppenderRef('stdout'))
|
50
|
+
|
51
|
+
# create a rolling file appender
|
52
|
+
cron = config.newComponent('CronTriggeringPolicy')
|
53
|
+
cron = cron.addAttribute('schedule', '0 0 0 * * ?')
|
54
|
+
|
55
|
+
size = config.newComponent('SizeBasedTriggeringPolicy')
|
56
|
+
size = size.addAttribute('size', '100M')
|
57
|
+
|
58
|
+
policies = config.newComponent('Policies')
|
59
|
+
policies = policies.addComponent(cron)
|
60
|
+
policies = policies.addComponent(size)
|
61
|
+
|
62
|
+
appender = config.newAppender('rolling_file', 'RollingFile')
|
63
|
+
appender = appender.addAttribute('fileName', ServerLogFile)
|
64
|
+
appender = appender.addAttribute('filePattern', RollingLogFilePath)
|
65
|
+
appender = appender.add(layout)
|
66
|
+
appender = appender.addComponent(policies)
|
67
|
+
config.add(appender)
|
68
|
+
|
69
|
+
root_logger = root_logger.addAttribute('additivity', false)
|
70
|
+
root_logger = root_logger.add(config.newAppenderRef('rolling_file'))
|
71
|
+
config.add(root_logger)
|
72
|
+
|
73
|
+
logging_configuration = config.build()
|
74
|
+
ctx = org.apache.logging.log4j.core.config::Configurator.initialize(logging_configuration)
|
75
|
+
ctx.updateLoggers()
|
76
|
+
end
|
77
|
+
# rubocop: enable Metrics/AbcSize
|
78
|
+
# rubocop: enable Metrics/MethodLength
|
79
|
+
# def init_log4j
|
80
|
+
end
|
81
|
+
# module LogInitialization
|
82
|
+
|
83
|
+
::LogInitialization.init_log4j if defined? Java
|
84
|
+
|
85
|
+
# The Apache log4j Logger class
|
86
|
+
# rubocop: disable Style/ClassAndModuleChildren
|
87
|
+
class org.apache.logging.log4j.core::Logger
|
88
|
+
alias log4j_error error
|
89
|
+
def error(error_or_message, error = nil)
|
90
|
+
return extract_backtrace(error_or_message) if error.nil?
|
91
|
+
log4j_error(generate_message(error_or_message, error))
|
92
|
+
extract_backtrace(error)
|
93
|
+
end
|
94
|
+
|
95
|
+
def generate_message(error_or_message, error)
|
96
|
+
error_message = "#{error_or_message}: #{error.class.name}"
|
97
|
+
error_message << ": #{error.message}" if error.respond_to?(:message)
|
98
|
+
error_message
|
99
|
+
end
|
100
|
+
|
101
|
+
def extract_backtrace(error, default_result = nil)
|
102
|
+
log4j_error(error)
|
103
|
+
if error.respond_to?(:backtrace)
|
104
|
+
error.backtrace.each { |trace| log4j_error(trace) unless trace.nil? }
|
105
|
+
elsif error.respond_to?(:getStackTrace)
|
106
|
+
error.getStackTrace().each { |trace| log4j_error(trace) unless trace.nil? }
|
107
|
+
else
|
108
|
+
default_result
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
# rubocop: enable Style/ClassAndModuleChildren
|
113
|
+
|
114
|
+
# The Logging module
|
115
|
+
module Logging
|
116
|
+
# rubocop: disable Style/MutableConstant
|
117
|
+
Configuration = {
|
118
|
+
level: Logger::INFO
|
119
|
+
}
|
120
|
+
# rubocop: enable Style/MutableConstant
|
121
|
+
|
122
|
+
def self.log_level=(log_level)
|
123
|
+
Logging::Configuration[:level] = log_level
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.log_level
|
127
|
+
Logging::Configuration[:level]
|
128
|
+
end
|
129
|
+
|
130
|
+
def log(level = Logging.log_level, log_name = nil)
|
131
|
+
@log ||= init_logger(level, log_name)
|
132
|
+
end
|
133
|
+
alias logger log
|
134
|
+
|
135
|
+
protected
|
136
|
+
|
137
|
+
def init_logger(level = Logging.log_level, logger_name = nil)
|
138
|
+
return init_java_logger(level, logger_name, caller[2]) if defined?(Java)
|
139
|
+
Logging.init_ruby_logger(level, logger_name)
|
140
|
+
end
|
141
|
+
|
142
|
+
def init_ruby_logger(level, logger_name = nil)
|
143
|
+
logger_instance = Logger.new(logger_name)
|
144
|
+
logger_instance.level = level
|
145
|
+
logger_instance
|
146
|
+
end
|
147
|
+
|
148
|
+
# rubocop: disable Metrics/AbcSize
|
149
|
+
def init_java_logger(level, logger_name = nil, source_location = nil)
|
150
|
+
logger_name = get_formatted_logger_name(logger_name)
|
151
|
+
logger_name = source_location.split(/\//).last if logger_name.empty?
|
152
|
+
level_name = symbolize_numeric_log_level(level).to_s.upcase
|
153
|
+
logger_instance = org.apache.logging.log4j.LogManager.getLogger(logger_name)
|
154
|
+
logger_instance.level = org.apache.logging.log4j.Level.to_level(level_name)
|
155
|
+
logger_instance
|
156
|
+
end
|
157
|
+
# rubocop: enable Metrics/AbcSize
|
158
|
+
|
159
|
+
def get_formatted_logger_name(logger_name = nil)
|
160
|
+
return logger_name.to_s[/\w+$/] unless logger_name.nil?
|
161
|
+
return name[/\w+$/] if is_a?(Class) || is_a?(Module)
|
162
|
+
self.class.name[/\w+$/]
|
163
|
+
end
|
164
|
+
|
165
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
166
|
+
# OFF: 0
|
167
|
+
# FATAL: 100
|
168
|
+
# ERROR: 200
|
169
|
+
# WARN: 300
|
170
|
+
# INFO: 400
|
171
|
+
# DEBUG: 500
|
172
|
+
# TRACE: 600
|
173
|
+
# ALL: 2147483647
|
174
|
+
# See: https://logging.apache.org/log4j/2.x/log4j-api/apidocs/org/apache/logging/log4j/Level.html
|
175
|
+
def symbolize_numeric_log_level(level)
|
176
|
+
case level
|
177
|
+
when 5..Float::INFINITY then :off
|
178
|
+
when 4 then :fatal
|
179
|
+
when 3 then :error
|
180
|
+
when 2 then :warn
|
181
|
+
when 1 then :info
|
182
|
+
when 0 then :debug
|
183
|
+
when -1 then :trace
|
184
|
+
when -2..-Float::INFINITY then :all
|
185
|
+
end
|
186
|
+
end
|
187
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
188
|
+
end
|
189
|
+
# module Logging
|
190
|
+
|
191
|
+
# The Module class
|
192
|
+
class Module
|
193
|
+
# Universally include Logging
|
194
|
+
include ::Logging
|
195
|
+
end
|
196
|
+
|
197
|
+
# The Class class
|
198
|
+
class Class
|
199
|
+
# Universally include Logging
|
200
|
+
include ::Logging
|
201
|
+
end
|
202
|
+
|
203
|
+
# The Object class
|
204
|
+
class Object
|
205
|
+
# Universally include Logging
|
206
|
+
include ::Logging
|
207
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
|
4
|
+
# -*- mode: ruby -*-
|
5
|
+
# vi: set ft=ruby :
|
6
|
+
|
7
|
+
# =begin
|
8
|
+
#
|
9
|
+
# Copyright Nels Nelson 2016-2022 but freely usable (see license)
|
10
|
+
#
|
11
|
+
# =end
|
12
|
+
|
13
|
+
require 'optparse'
|
14
|
+
|
15
|
+
require_relative 'config'
|
16
|
+
|
17
|
+
# The Server module
|
18
|
+
module Server
|
19
|
+
# The ArgumentsParser class
|
20
|
+
class ArgumentsParser
|
21
|
+
Flags = %i[
|
22
|
+
banner port ssl idle_reading idle_writing log_requests log_level help
|
23
|
+
version
|
24
|
+
].freeze
|
25
|
+
attr_reader :parser, :options
|
26
|
+
|
27
|
+
def initialize(option_parser = OptionParser.new)
|
28
|
+
@parser = option_parser
|
29
|
+
@options = ::Server::Config::DEFAULTS.dup
|
30
|
+
Flags.each { |method_name| method(method_name).call }
|
31
|
+
end
|
32
|
+
|
33
|
+
def banner
|
34
|
+
@parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [port] [options]"
|
35
|
+
@parser.separator ''
|
36
|
+
@parser.separator 'Options:'
|
37
|
+
end
|
38
|
+
|
39
|
+
IntegerPattern = /^\d+$/.freeze
|
40
|
+
|
41
|
+
def validated_port(val)
|
42
|
+
raise "Invalid port: #{v}" unless IntegerPattern.match?(val.to_s) && val.positive? && val < 65_536
|
43
|
+
val
|
44
|
+
end
|
45
|
+
|
46
|
+
def port
|
47
|
+
description = "Port on which to listen for connections; default: #{@options[:port]}"
|
48
|
+
@parser.on('-p', '--port=<port>', Integer, description) do |v|
|
49
|
+
@options[:port] = validated_port(v).to_i
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def ssl
|
54
|
+
@parser.on('-s', '--ssl', "Enable SSL socket server; default: #{@options[:ssl]}") do
|
55
|
+
@options[:ssl] = true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def idle_reading
|
60
|
+
@parser.on('--idle-reading=seconds', 'Amount of time channel can idle without incoming data') do |v|
|
61
|
+
@options[:idle_reading] = v.to_i
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def idle_writing
|
66
|
+
@parser.on('--idle-writing=seconds', 'Amount of time channel can idle without outgoing data') do |v|
|
67
|
+
@options[:idle_writing] = v.to_i
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def log_requests
|
72
|
+
@parser.on('-r', '--log-requests', 'Include individual request info in log output') do
|
73
|
+
@options[:log_requests] = true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def log_level
|
78
|
+
@parser.on_tail('-v', '--verbose', 'Increase verbosity') do
|
79
|
+
@options[:log_level] -= 1
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def help
|
84
|
+
@parser.on_tail('-?', '--help', 'Show this message') do
|
85
|
+
puts @parser
|
86
|
+
exit
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def version
|
91
|
+
@parser.on_tail('--version', 'Show version') do
|
92
|
+
puts "#{File.basename($PROGRAM_NAME)} version #{::Server::VERSION}"
|
93
|
+
exit
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
# class ArgumentsParser
|
98
|
+
|
99
|
+
def parse_arguments(arguments_parser = ::Server::ArgumentsParser.new)
|
100
|
+
arguments_parser.parser.parse!(ARGV)
|
101
|
+
arguments_parser.options
|
102
|
+
rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
|
103
|
+
abort e.message
|
104
|
+
end
|
105
|
+
end
|
106
|
+
# module Server
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
|
4
|
+
# -*- mode: ruby -*-
|
5
|
+
# vi: set ft=ruby :
|
6
|
+
|
7
|
+
# =begin
|
8
|
+
#
|
9
|
+
# Copyright Nels Nelson 2016-2022 but freely usable (see license)
|
10
|
+
#
|
11
|
+
# =end
|
12
|
+
|
13
|
+
require 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
require_relative 'message_handler'
|
17
|
+
require_relative 'modular_handler'
|
18
|
+
|
19
|
+
# The Server module
|
20
|
+
module Server
|
21
|
+
java_import Java::io.netty.handler.codec.DelimiterBasedFrameDecoder
|
22
|
+
java_import Java::io.netty.handler.codec.Delimiters
|
23
|
+
java_import Java::io.netty.handler.codec.string.StringDecoder
|
24
|
+
java_import Java::io.netty.handler.codec.string.StringEncoder
|
25
|
+
java_import Java::io.netty.handler.ssl.SslContextBuilder
|
26
|
+
java_import Java::io.netty.handler.ssl.SslHandler
|
27
|
+
java_import Java::io.netty.handler.ssl.util.InsecureTrustManagerFactory
|
28
|
+
java_import Java::io.netty.handler.ssl.util.SelfSignedCertificate
|
29
|
+
java_import Java::io.netty.util.concurrent.FutureListener
|
30
|
+
|
31
|
+
# The ChannelInitializer class
|
32
|
+
class ChannelInitializer < Java::io.netty.channel.ChannelInitializer
|
33
|
+
DefaultHandler = ModularHandler.new
|
34
|
+
FrameDecoderBufferBytesSize = 8192
|
35
|
+
# The encoder and decoder are sharable. If they were not, then
|
36
|
+
# constant definitions could not be used.
|
37
|
+
Decoder = StringDecoder.new
|
38
|
+
Encoder = StringEncoder.new
|
39
|
+
attr_accessor :handlers
|
40
|
+
attr_reader :options
|
41
|
+
|
42
|
+
def initialize(options = {})
|
43
|
+
super()
|
44
|
+
@options = options
|
45
|
+
@handlers = []
|
46
|
+
end
|
47
|
+
|
48
|
+
def <<(handler)
|
49
|
+
@handlers << handler
|
50
|
+
end
|
51
|
+
|
52
|
+
def initChannel(channel)
|
53
|
+
pipeline = channel.pipeline
|
54
|
+
pipeline.addLast(ssl_handler(channel)) if @options[:ssl]
|
55
|
+
pipeline.addLast(
|
56
|
+
DelimiterBasedFrameDecoder.new(FrameDecoderBufferBytesSize, Delimiters.lineDelimiter),
|
57
|
+
Decoder,
|
58
|
+
Encoder
|
59
|
+
)
|
60
|
+
add_user_handlers(pipeline)
|
61
|
+
pipeline.addLast(DefaultHandler)
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
def add_user_handlers(pipeline)
|
67
|
+
@handlers.each do |handler|
|
68
|
+
case handler
|
69
|
+
when Class then pipeline.addLast(handler.new)
|
70
|
+
when Proc then pipeline.addLast(Server::MessageHandler.new(&handler))
|
71
|
+
else pipeline.addLast(handler)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def ssl_context
|
79
|
+
return @ssl_ctx unless @ssl_ctx.nil?
|
80
|
+
log.debug 'Initializing SSL context'
|
81
|
+
require 'bouncycastle'
|
82
|
+
ssc = SelfSignedCertificate.new
|
83
|
+
builder = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
|
84
|
+
@ssl_ctx = builder.trustManager(InsecureTrustManagerFactory::INSTANCE).build()
|
85
|
+
end
|
86
|
+
|
87
|
+
def ssl_handler_instance(channel, ssl_ctx = ssl_context)
|
88
|
+
return ssl_ctx.newHandler(channel.alloc()) if ssl_ctx.respond_to?(:newHandler)
|
89
|
+
log.warn 'The SSL context did not provide a handler initializer'
|
90
|
+
log.warn 'Creating handler with SSL engine of the context'
|
91
|
+
SslHandler.new(ssl_engine(ssl_ctx))
|
92
|
+
end
|
93
|
+
|
94
|
+
def ssl_handler(channel)
|
95
|
+
handler = ssl_handler_instance(channel)
|
96
|
+
handler.handshake_future.addListener(SslHandshakeFutureListener.new)
|
97
|
+
handler
|
98
|
+
end
|
99
|
+
|
100
|
+
# The SslHandshakeFutureListener class
|
101
|
+
class SslHandshakeFutureListener
|
102
|
+
include FutureListener
|
103
|
+
# @Override
|
104
|
+
#
|
105
|
+
# public void operationComplete(Future<Channel> future) throws Exception
|
106
|
+
def operationComplete(future)
|
107
|
+
raise future.cause unless future.success?
|
108
|
+
session = future.now.pipeline.get('SslHandler#0')&.engine&.session
|
109
|
+
::Server.log.info "Channel protocol: #{session.protocol}, cipher suite: #{session.cipher_suite}"
|
110
|
+
rescue StandardError => e
|
111
|
+
::Server.log.warn e.message
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def ssl_engine(ssl_ctx)
|
116
|
+
ssl_engine = ssl_ctx.createSSLEngine()
|
117
|
+
ssl_engine.setUseClientMode(false) # Server mode
|
118
|
+
ssl_engine.setNeedClientAuth(false)
|
119
|
+
ssl_engine
|
120
|
+
end
|
121
|
+
end
|
122
|
+
# class ChannelInitializer
|
123
|
+
end
|
124
|
+
# module Server
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
|
4
|
+
# -*- mode: ruby -*-
|
5
|
+
# vi: set ft=ruby :
|
6
|
+
|
7
|
+
# =begin
|
8
|
+
#
|
9
|
+
# Copyright Nels Nelson 2016-2022 but freely usable (see license)
|
10
|
+
#
|
11
|
+
# =end
|
12
|
+
|
13
|
+
require 'logger'
|
14
|
+
|
15
|
+
# The Server module
|
16
|
+
module Server
|
17
|
+
# The Config module
|
18
|
+
module Config
|
19
|
+
DEFAULTS = {
|
20
|
+
host: '0.0.0.0',
|
21
|
+
port: 8080,
|
22
|
+
ssl: false,
|
23
|
+
idle_reading: 5 * 60, # seconds
|
24
|
+
idle_writing: 30, # seconds
|
25
|
+
log_requests: false,
|
26
|
+
log_level: Logger::INFO,
|
27
|
+
quit_commands: %i[bye quit]
|
28
|
+
}.freeze
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
|
4
|
+
# -*- mode: ruby -*-
|
5
|
+
# vi: set ft=ruby :
|
6
|
+
|
7
|
+
# =begin
|
8
|
+
#
|
9
|
+
# Copyright Nels Nelson 2016-2022 but freely usable (see license)
|
10
|
+
#
|
11
|
+
# =end
|
12
|
+
|
13
|
+
require 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
require_relative 'channel_initializer'
|
17
|
+
require_relative 'shutdown_hook'
|
18
|
+
|
19
|
+
# The Server module
|
20
|
+
module Server
|
21
|
+
java_import Java::io.netty.bootstrap.ServerBootstrap
|
22
|
+
java_import Java::io.netty.channel.ChannelOption
|
23
|
+
java_import Java::io.netty.channel.nio.NioEventLoopGroup
|
24
|
+
java_import Java::io.netty.handler.logging.LogLevel
|
25
|
+
java_import Java::io.netty.handler.logging.LoggingHandler
|
26
|
+
|
27
|
+
# The InstanceMethods module
|
28
|
+
module InstanceMethods
|
29
|
+
def configure_handlers(&block)
|
30
|
+
::Server::ChannelInitializer::DefaultHandler.add_listener(self)
|
31
|
+
channel_initializer << block if block_given?
|
32
|
+
end
|
33
|
+
|
34
|
+
def bootstrap
|
35
|
+
@bootstrap = ServerBootstrap.new
|
36
|
+
@bootstrap.group(boss_group, worker_group)
|
37
|
+
@bootstrap.channel(::Server::CHANNEL_TYPE)
|
38
|
+
@bootstrap.option(ChannelOption::SO_BACKLOG, 100.to_java(java.lang.Integer))
|
39
|
+
@bootstrap.handler(logging_handler) if options[:log_requests]
|
40
|
+
@bootstrap.childHandler(channel_initializer)
|
41
|
+
end
|
42
|
+
|
43
|
+
def channel_initializer
|
44
|
+
@channel_initializer ||= ::Server::ChannelInitializer.new(@options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def boss_group
|
48
|
+
@boss_group ||= NioEventLoopGroup.new(2)
|
49
|
+
end
|
50
|
+
|
51
|
+
def worker_group
|
52
|
+
@worker_group ||= NioEventLoopGroup.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def logging_handler
|
56
|
+
@logging_handler ||= LoggingHandler.new(LogLevel::INFO)
|
57
|
+
end
|
58
|
+
|
59
|
+
# rubocop: disable Metrics/AbcSize
|
60
|
+
# rubocop: disable Metrics/MethodLength
|
61
|
+
def run(port = @options[:port])
|
62
|
+
channel = bootstrap.bind(port).sync().channel()
|
63
|
+
::Server::Channels.add(channel)
|
64
|
+
::Server::ShutdownHook.new(self)
|
65
|
+
log.info "Listening on #{channel.local_address}"
|
66
|
+
channel.closeFuture().sync()
|
67
|
+
rescue java.lang.IllegalArgumentException => e
|
68
|
+
raise "Invalid argument: #{e.message}"
|
69
|
+
rescue java.net.BindException => e
|
70
|
+
raise "Bind error: #{e.message}: #{options[:host]}:#{port}"
|
71
|
+
rescue java.net.SocketException => e
|
72
|
+
raise "Socket error: #{e.message}: #{options[:host]}:#{port}"
|
73
|
+
ensure
|
74
|
+
boss_group&.shutdownGracefully()
|
75
|
+
worker_group&.shutdownGracefully()
|
76
|
+
end
|
77
|
+
# rubocop: enable Metrics/AbcSize
|
78
|
+
# rubocop: enable Metrics/MethodLength
|
79
|
+
|
80
|
+
def shutdown
|
81
|
+
::Server::Channels.disconnect().awaitUninterruptibly()
|
82
|
+
::Server::Channels.close().awaitUninterruptibly()
|
83
|
+
end
|
84
|
+
|
85
|
+
def <<(handler)
|
86
|
+
channel_initializer << handler
|
87
|
+
end
|
88
|
+
end
|
89
|
+
# module ServerInstanceMethods
|
90
|
+
end
|
91
|
+
# module Server
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
|
4
|
+
# -*- mode: ruby -*-
|
5
|
+
# vi: set ft=ruby :
|
6
|
+
|
7
|
+
# =begin
|
8
|
+
#
|
9
|
+
# Copyright Nels Nelson 2016-2022 but freely usable (see license)
|
10
|
+
#
|
11
|
+
# =end
|
12
|
+
|
13
|
+
require 'java'
|
14
|
+
|
15
|
+
# The Server module
|
16
|
+
module Server
|
17
|
+
# The Listenable module
|
18
|
+
module Listenable
|
19
|
+
def listeners
|
20
|
+
@listeners ||= java.util.concurrent.CopyOnWriteArrayList.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_listener(listener)
|
24
|
+
listeners << listener
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove_listener(listener)
|
28
|
+
listeners.delete(listener)
|
29
|
+
end
|
30
|
+
|
31
|
+
def notify(message, *args)
|
32
|
+
return if listeners.empty?
|
33
|
+
log.trace "Notifying listeners (#{listeners}) of message: #{message}"
|
34
|
+
listeners.each do |listener|
|
35
|
+
listener.send(message.to_sym, *args) if listener.respond_to?(message.to_sym)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
# module Listenable
|
40
|
+
end
|
41
|
+
# module Server
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
|
4
|
+
# -*- mode: ruby -*-
|
5
|
+
# vi: set ft=ruby :
|
6
|
+
|
7
|
+
# =begin
|
8
|
+
#
|
9
|
+
# Copyright Nels Nelson 2016-2022 but freely usable (see license)
|
10
|
+
#
|
11
|
+
# =end
|
12
|
+
|
13
|
+
require 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
# The Server module
|
17
|
+
module Server
|
18
|
+
java_import Java::io.netty.channel.ChannelFutureListener
|
19
|
+
java_import Java::io.netty.channel.ChannelHandler
|
20
|
+
java_import Java::io.netty.channel.SimpleChannelInboundHandler
|
21
|
+
|
22
|
+
# The MessageHandler class
|
23
|
+
class MessageHandler < SimpleChannelInboundHandler
|
24
|
+
include ChannelHandler
|
25
|
+
include ChannelFutureListener
|
26
|
+
|
27
|
+
def initialize(&handler)
|
28
|
+
super()
|
29
|
+
@handler = handler
|
30
|
+
end
|
31
|
+
|
32
|
+
# Please keep in mind that this method will be renamed to
|
33
|
+
# messageReceived(ChannelHandlerContext, I) in 5.0.
|
34
|
+
#
|
35
|
+
# java_signature 'protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception'
|
36
|
+
def channelRead0(ctx, msg)
|
37
|
+
log.trace "##{__method__} channel: #{ctx.channel}, message: #{msg.inspect}"
|
38
|
+
messageReceived(ctx, msg)
|
39
|
+
end
|
40
|
+
|
41
|
+
def messageReceived(ctx, msg)
|
42
|
+
log.trace "##{__method} channel: #{ctx.channel}, message: #{msg.inspect}"
|
43
|
+
msg&.chomp!
|
44
|
+
log.info "Received message: #{msg}"
|
45
|
+
return super(ctx, msg) unless respond_to?(:handle_message) && @handler.arity == 2
|
46
|
+
handle_message(ctx, msg)
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_message(ctx, message)
|
50
|
+
request = message.to_s.strip
|
51
|
+
response = @handler.call(ctx.channel, request).to_s.chomp
|
52
|
+
log.debug "response: #{response}"
|
53
|
+
ctx.channel.writeAndFlush("#{response}\n")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|