tcp-server 1.0.1-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|