fix-engine 0.0.24 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +151 -1
- data/bin/fix-engine +7 -1
- data/lib/fix/engine.rb +3 -2
- data/lib/fix/engine/client.rb +41 -10
- data/lib/fix/engine/client_connection.rb +50 -0
- data/lib/fix/engine/connection.rb +123 -151
- data/lib/fix/engine/logger.rb +7 -1
- data/lib/fix/engine/message_buffer.rb +41 -23
- data/lib/fix/engine/server.rb +12 -3
- data/lib/fix/engine/server_connection.rb +103 -0
- data/lib/fix/engine/version.rb +1 -1
- metadata +41 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 32ec2077ed212596bc692e4650be57c8a003a55c
|
4
|
+
data.tar.gz: e2ad9ef6e6824e8d06de895dd44747f01a10934f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3cced1625f0196a24a4006e1b9784fa5db83d9141167bca0286bab405fd0e6abefa5ebf0db81b6042ec66fd4824e48b5d5ad1d4e0dd0d561a2bd56e9e075b46c
|
7
|
+
data.tar.gz: 36a2e95f782491cd465c5bf81029a84956884d85180c2beb08dff94652f261a43490d2bdd86feff1ec4bc2c770c99b886fc133ded5fc68b126f59e09710e1642
|
data/README.md
CHANGED
@@ -1,4 +1,154 @@
|
|
1
1
|
FIX Engine [![Build Status](https://secure.travis-ci.org/Paymium/fix-engine.png?branch=master)](http://travis-ci.org/Paymium/fix-engine)
|
2
2
|
=
|
3
3
|
|
4
|
-
This library provides an event-machine based FIX server.
|
4
|
+
This library provides an event-machine based FIX server and client connection implementation.
|
5
|
+
|
6
|
+
# Usage as a FIX client
|
7
|
+
|
8
|
+
## Implement a connection handler
|
9
|
+
|
10
|
+
Create an `EM::Connection` subclass and include the `FE::ClientConnection` module. You will then have to implement
|
11
|
+
the callbacks for the various message types you are interested in.
|
12
|
+
|
13
|
+
````ruby
|
14
|
+
require 'fix/engine'
|
15
|
+
|
16
|
+
module Referee
|
17
|
+
|
18
|
+
class FixConnection < EM::Connection
|
19
|
+
|
20
|
+
include FE::ClientConnection
|
21
|
+
|
22
|
+
attr_accessor :exchange
|
23
|
+
|
24
|
+
#
|
25
|
+
# When a logon message is received we request a market snapshot
|
26
|
+
# and subscribe for continuous updates
|
27
|
+
#
|
28
|
+
def on_logon(msg)
|
29
|
+
mdr = FP::Messages::MarketDataRequest.new
|
30
|
+
|
31
|
+
mdr.md_req_id = 'foo'
|
32
|
+
|
33
|
+
mdr.subscription_request_type = :updates
|
34
|
+
mdr.market_depth = :full
|
35
|
+
mdr.md_update_type = :incremental
|
36
|
+
|
37
|
+
mdr.instruments.build do |i|
|
38
|
+
i.symbol = 'EUR/XBT'
|
39
|
+
end
|
40
|
+
|
41
|
+
[:bid, :ask, :trade, :open, :vwap, :close].each do |mdet|
|
42
|
+
mdr.md_entry_types.build do |m|
|
43
|
+
m.md_entry_type = mdet
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
send_msg(mdr)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Called when a market data snapshot is received
|
51
|
+
def on_market_data_snapshot(msg)
|
52
|
+
update_book_with(msg)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Called upon each subsequent update
|
56
|
+
def on_market_data_incremental_refresh(msg)
|
57
|
+
update_book_with(msg)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Update the local order book copy with the new data
|
61
|
+
def update_book_with(msg)
|
62
|
+
msg.md_entries.each do |mde|
|
63
|
+
exchange.book[mde.md_entry_type].set_depth_at(BigDecimal(mde.md_entry_px), BigDecimal(mde.md_entry_size))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
````
|
70
|
+
|
71
|
+
## Use it to connect to a FIX acceptor
|
72
|
+
|
73
|
+
Once your connection class has been created you can establish a connection in a running EventMachine reactor.
|
74
|
+
See the [referee gem](https://github.com/davout/referee) for the full code example.
|
75
|
+
|
76
|
+
````ruby
|
77
|
+
require 'referee/exchange'
|
78
|
+
require 'referee/fix_connection'
|
79
|
+
|
80
|
+
module Referee
|
81
|
+
module Exchanges
|
82
|
+
class Paymium < Referee::Exchange
|
83
|
+
|
84
|
+
FIX_SERVER = 'fix.paymium.com'
|
85
|
+
FIX_PORT = 8359
|
86
|
+
|
87
|
+
def symbol
|
88
|
+
'PAYM'
|
89
|
+
end
|
90
|
+
|
91
|
+
def currency
|
92
|
+
'EUR'
|
93
|
+
end
|
94
|
+
|
95
|
+
def connect
|
96
|
+
FE::Logger.logger.level = Logger::WARN
|
97
|
+
|
98
|
+
EM.connect(FIX_SERVER, FIX_PORT, FixConnection) do |conn|
|
99
|
+
conn.target_comp_id = 'PAYMIUM'
|
100
|
+
conn.comp_id = 'BC-U458625'
|
101
|
+
conn.username = 'BC-U458625'
|
102
|
+
conn.exchange = self
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
````
|
109
|
+
|
110
|
+
|
111
|
+
# Usage as a FIX acceptor
|
112
|
+
|
113
|
+
You can start a simple FIX acceptor that will maintain a session by running the `fix-engine` executable.
|
114
|
+
The basic FIX acceptor requires a `COMP_ID` environment variable to be set.
|
115
|
+
|
116
|
+
````
|
117
|
+
$ COMP_ID=MY_COMP_ID fix-engine
|
118
|
+
|
119
|
+
> D, [2015-01-07T12:47:07.807867 #87486] DEBUG -- : Starting FIX engine v0.0.31, listening on <0.0.0.0:8359>, exit with <Ctrl-C>
|
120
|
+
> D, [2015-01-07T12:47:12.379787 #87486] DEBUG -- : Client connected <127.0.0.1:54204>, expecting logon message in the next 10s
|
121
|
+
> D, [2015-01-07T12:47:12.816626 #87486] DEBUG -- : Received a <Fix::Protocol::Messages::Logon> from <127.0.0.1:54204> with sequence number <1>
|
122
|
+
> D, [2015-01-07T12:47:12.820093 #87486] DEBUG -- : Peer authenticated as <JAVA_TESTS> with heartbeat interval of <30s> and message sequence number start <1>
|
123
|
+
> D, [2015-01-07T12:47:12.820259 #87486] DEBUG -- : Heartbeat interval for <127.0.0.1:54204> : <30s>
|
124
|
+
> D, [2015-01-07T12:47:12.820899 #87486] DEBUG -- : Sending <Fix::Protocol::Messages::Logon> to <127.0.0.1:54204> with sequence number <1>
|
125
|
+
````
|
126
|
+
|
127
|
+
In order to handle business messages appropriately you need to implement a connection handler
|
128
|
+
that includes the `FE::ServerConnection` module and use that as a connection handler.
|
129
|
+
|
130
|
+
````ruby
|
131
|
+
class MyHandler < EM::Connection
|
132
|
+
|
133
|
+
include FE::ServerConnection
|
134
|
+
|
135
|
+
def on_market_data_request
|
136
|
+
# Fetch market data and send the relevant response
|
137
|
+
# ...
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
server = FE::Server.new('127.0.0.1', 8095, MyHandler) do |conn|
|
143
|
+
conn.comp_id = 'MY_COMP_ID'
|
144
|
+
end
|
145
|
+
|
146
|
+
# This will also start an EventMachine reactor
|
147
|
+
server.run!
|
148
|
+
|
149
|
+
# This would be used inside an already-running reactor
|
150
|
+
EM.run do
|
151
|
+
server.start_server
|
152
|
+
end
|
153
|
+
````
|
154
|
+
|
data/bin/fix-engine
CHANGED
@@ -5,5 +5,11 @@ require 'fix/engine'
|
|
5
5
|
ip = ENV['FE_IP'] || FE::DEFAULT_IP
|
6
6
|
port = ENV['FE_PORT'] || FE::DEFAULT_PORT
|
7
7
|
|
8
|
-
|
8
|
+
if ENV['COMP_ID']
|
9
|
+
Fix::Engine.run!(ip, port) do |conn|
|
10
|
+
conn.comp_id = ENV['COMP_ID']
|
11
|
+
end
|
12
|
+
else
|
13
|
+
puts "The COMP_ID environment variable must be set to be able to start a basic acceptor."
|
14
|
+
end
|
9
15
|
|
data/lib/fix/engine.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'fix/engine/logger'
|
2
2
|
require 'fix/engine/server'
|
3
|
+
require 'fix/engine/client_connection'
|
3
4
|
|
4
5
|
#
|
5
6
|
# Main FIX namespace
|
@@ -20,8 +21,8 @@ module Fix
|
|
20
21
|
#
|
21
22
|
# Runs a FIX server engine
|
22
23
|
#
|
23
|
-
def self.run!(ip = DEFAULT_IP, port = DEFAULT_PORT, handler = FE::
|
24
|
-
Server.new(ip, port, handler).run!
|
24
|
+
def self.run!(ip = DEFAULT_IP, port = DEFAULT_PORT, handler = FE::ServerConnection, &block)
|
25
|
+
Server.new(ip, port, handler, &block).run!
|
25
26
|
end
|
26
27
|
|
27
28
|
#
|
data/lib/fix/engine/client.rb
CHANGED
@@ -8,9 +8,9 @@ module Fix
|
|
8
8
|
#
|
9
9
|
class Client
|
10
10
|
|
11
|
-
|
11
|
+
@clients = {}
|
12
12
|
|
13
|
-
attr_accessor :ip, :port, :connection, :
|
13
|
+
attr_accessor :ip, :port, :connection, :username
|
14
14
|
|
15
15
|
include Logger
|
16
16
|
|
@@ -18,34 +18,65 @@ module Fix
|
|
18
18
|
@ip = ip
|
19
19
|
@port = port
|
20
20
|
@connection = connection
|
21
|
+
|
22
|
+
self.class.instance_variable_get(:@clients)[key] = self
|
21
23
|
end
|
22
24
|
|
23
25
|
#
|
24
26
|
# Returns a client instance from its connection IP
|
25
27
|
#
|
26
28
|
# @param ip [String] The connection IP
|
29
|
+
# @param port [Fixnum] The connection port
|
30
|
+
# @param connection [FE::Connection] Optionnally the connection which will used to create an instance if none exists
|
27
31
|
# @return [Fix::Engine::Client] The client connected for this IP
|
28
32
|
#
|
29
|
-
def self.get(ip, port, connection)
|
30
|
-
|
33
|
+
def self.get(ip, port, connection = nil)
|
34
|
+
@clients[key(ip, port)] || Client.new(ip, port, connection)
|
31
35
|
end
|
32
36
|
|
37
|
+
#
|
38
|
+
# Returns the count of currently connected clients
|
39
|
+
#
|
40
|
+
# @return [Fixnum] The client count
|
41
|
+
#
|
33
42
|
def self.count
|
34
|
-
|
43
|
+
@clients.count
|
35
44
|
end
|
36
45
|
|
46
|
+
#
|
47
|
+
# Removes a client from the currently connected ones
|
48
|
+
#
|
49
|
+
# @param ip [String] The client's remote IP
|
50
|
+
# @param port [Fixnum] The client's port
|
51
|
+
#
|
37
52
|
def self.delete(ip, port)
|
38
|
-
|
39
|
-
end
|
40
|
-
|
41
|
-
def has_session?
|
42
|
-
!!client_id
|
53
|
+
@clients.delete(key(ip, port))
|
43
54
|
end
|
44
55
|
|
56
|
+
#
|
57
|
+
# Returns an identifier for the current client
|
58
|
+
#
|
59
|
+
# @return [String] An identifier
|
60
|
+
#
|
45
61
|
def key
|
46
62
|
self.class.key(ip, port)
|
47
63
|
end
|
48
64
|
|
65
|
+
#
|
66
|
+
# Removes the current client from the array of connected ones
|
67
|
+
#
|
68
|
+
def delete
|
69
|
+
self.class.delete(ip, port)
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Returns an identifier for the given IP and port
|
74
|
+
#
|
75
|
+
# @param ip [String] The client's remote IP
|
76
|
+
# @param port [Fixnum] The client's port
|
77
|
+
#
|
78
|
+
# @return [String] An identifier
|
79
|
+
#
|
49
80
|
def self.key(ip, port)
|
50
81
|
"#{ip}:#{port}"
|
51
82
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'fix/engine/connection'
|
2
|
+
|
3
|
+
module Fix
|
4
|
+
module Engine
|
5
|
+
|
6
|
+
#
|
7
|
+
# The client connection wrapper, used in order to connect a remote FIX server
|
8
|
+
#
|
9
|
+
module ClientConnection
|
10
|
+
|
11
|
+
include Connection
|
12
|
+
|
13
|
+
attr_accessor :username
|
14
|
+
|
15
|
+
#
|
16
|
+
# Run after we've connected to the server
|
17
|
+
#
|
18
|
+
def post_init
|
19
|
+
super
|
20
|
+
|
21
|
+
log("Connecting to server sending a logon message with our COMP_ID <#{@comp_id}>")
|
22
|
+
|
23
|
+
@logged_in = false
|
24
|
+
|
25
|
+
EM.next_tick { send_logon }
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Sends a logon message to the server we're connected to
|
30
|
+
#
|
31
|
+
def send_logon
|
32
|
+
logon = FP::Messages::Logon.new
|
33
|
+
logon.username = @username
|
34
|
+
logon.target_comp_id = @peer_comp_id
|
35
|
+
logon.sender_comp_id = @comp_id
|
36
|
+
logon.reset_seq_num_flag = true
|
37
|
+
send_msg(logon)
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Consider ourselves logged-in if we receive on of these
|
42
|
+
#
|
43
|
+
def on_logon(msg)
|
44
|
+
@logged_in = true
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'eventmachine'
|
2
2
|
|
3
3
|
require 'fix/engine/message_buffer'
|
4
|
-
require 'fix/engine/client'
|
5
4
|
|
6
5
|
module Fix
|
7
6
|
module Engine
|
@@ -9,61 +8,52 @@ module Fix
|
|
9
8
|
#
|
10
9
|
# The client connection handling logic and method overrides
|
11
10
|
#
|
12
|
-
|
11
|
+
module Connection
|
13
12
|
|
14
13
|
include Logger
|
15
14
|
|
16
|
-
#
|
17
|
-
# Timespan during which a client must send a logon message after connecting
|
18
|
-
#
|
19
|
-
LOGON_TIMEOUT = 10
|
20
|
-
|
21
|
-
#
|
22
|
-
# Timespan during which we will wait for a heartbeat response from the client
|
23
|
-
#
|
24
|
-
HRTBT_TIMEOUT = 10
|
25
|
-
|
26
15
|
#
|
27
16
|
# Grace time before we disconnect a client that doesn't reply to a test request
|
28
17
|
#
|
29
18
|
TEST_REQ_GRACE_TIME = 15
|
30
19
|
|
31
|
-
attr_accessor :ip, :port, :
|
20
|
+
attr_accessor :ip, :port, :msg_buf, :hrtbt_int, :last_request_at, :comp_id, :target_comp_id
|
32
21
|
|
33
22
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
DEFAULT_COMP_ID = 'PYMBTC'
|
37
|
-
|
38
|
-
#
|
39
|
-
# Run after a client has connected
|
23
|
+
# Initialize the messages array, our comp_id, and the expected message sequence number
|
40
24
|
#
|
41
25
|
def post_init
|
42
|
-
@
|
43
|
-
@client = Client.get(ip, port, self)
|
44
|
-
@expected_clt_seq_num = 1
|
26
|
+
@expected_seq_num = 1
|
45
27
|
|
46
|
-
log("Client connected from <#{@client.key}>, expecting logon message in the next #{LOGON_TIMEOUT}s")
|
47
|
-
|
48
|
-
@comp_id ||= DEFAULT_COMP_ID
|
49
|
-
|
50
28
|
# The sent messages
|
51
29
|
@messages = []
|
30
|
+
end
|
52
31
|
|
53
|
-
|
54
|
-
|
55
|
-
|
32
|
+
#
|
33
|
+
# The way we refer to our connection peer in various logs and messages
|
34
|
+
#
|
35
|
+
def peer
|
36
|
+
"server"
|
56
37
|
end
|
57
38
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
39
|
+
#
|
40
|
+
# Sets the heartbeat interval and schedules the keep alive call
|
41
|
+
#
|
42
|
+
# @param interval [Fixnum] The frequency in seconds at which a heartbeat should be emitted
|
43
|
+
#
|
44
|
+
def set_heartbeat_interval(interval)
|
45
|
+
@hrtbt_int && raise("Can't set heartbeat interval twice")
|
46
|
+
@hrtbt_int = interval
|
47
|
+
|
48
|
+
log("Heartbeat interval for #{peer} : <#{hrtbt_int}s>")
|
49
|
+
@keep_alive_timer = EM.add_periodic_timer(1) { keep_alive }
|
64
50
|
end
|
65
51
|
|
66
|
-
|
52
|
+
#
|
53
|
+
# Keeps the connection alive by sending regular heartbeats, and test request
|
54
|
+
# messages whenever the connection has been idl'ing for too long
|
55
|
+
#
|
56
|
+
def keep_alive
|
67
57
|
@last_send_at ||= 0
|
68
58
|
@last_request_at ||= 0
|
69
59
|
@hrtbt_int ||= 0
|
@@ -75,25 +65,48 @@ module Fix
|
|
75
65
|
|
76
66
|
# Trigger a test req message when we haven't received anything for a while
|
77
67
|
if !@pending_test_req_id && (last_request_at < (Time.now.to_i - @hrtbt_int))
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
@pending_test_req_id = tr.test_req_id
|
68
|
+
send_test_request
|
69
|
+
end
|
70
|
+
end
|
82
71
|
|
83
|
-
|
84
|
-
|
85
|
-
|
72
|
+
#
|
73
|
+
# Sends a test request and expects an answer before +TEST_REQ_GRACE_TIME+
|
74
|
+
#
|
75
|
+
def send_test_request
|
76
|
+
tr = FP::Messages::TestRequest.new
|
77
|
+
tr.test_req_id = SecureRandom.hex(6)
|
78
|
+
send_msg(tr)
|
79
|
+
@pending_test_req_id = tr.test_req_id
|
80
|
+
|
81
|
+
EM.add_timer(TEST_REQ_GRACE_TIME) do
|
82
|
+
@pending_test_req_id && kill!
|
86
83
|
end
|
87
84
|
end
|
88
85
|
|
86
|
+
#
|
87
|
+
# Sends a heartbeat message with an optional +test_req_id+ parameter
|
88
|
+
#
|
89
|
+
# @param test_req_id [String] Sets the test request ID if sent in response to a test request
|
90
|
+
#
|
91
|
+
def send_heartbeat(test_req_id = nil)
|
92
|
+
msg = FP::Messages::Heartbeat.new
|
93
|
+
test_req_id && msg.test_req_id = test_req_id
|
94
|
+
send_msg(msg)
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Sends a +Fix::Protocol::Message+ to the connected peer
|
99
|
+
#
|
100
|
+
# @param msg [Fix::Protocol::Message] The message to send
|
101
|
+
#
|
89
102
|
def send_msg(msg)
|
90
103
|
@send_seq_num ||= 1
|
91
104
|
|
92
105
|
msg.msg_seq_num = @send_seq_num
|
93
106
|
msg.sender_comp_id = @comp_id
|
94
|
-
msg.target_comp_id
|
107
|
+
msg.target_comp_id = @target_comp_id
|
95
108
|
|
96
|
-
log("Sending <#{msg.class}> to
|
109
|
+
log("Sending <#{msg.class}> to #{peer} with sequence number <#{msg.msg_seq_num}>")
|
97
110
|
|
98
111
|
if msg.valid?
|
99
112
|
@messages[msg.msg_seq_num] = msg
|
@@ -102,85 +115,71 @@ module Fix
|
|
102
115
|
@last_send_at = Time.now.to_i
|
103
116
|
else
|
104
117
|
log(msg.errors.join(', '))
|
105
|
-
raise "Tried to send invalid message!"
|
118
|
+
raise "Tried to send invalid message! <#{msg.errors.join(', ')}>"
|
106
119
|
end
|
107
120
|
end
|
108
121
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
log("Heartbeat interval for <#{ip}:#{port}> : <#{hrtbt_int}s>")
|
114
|
-
@hrtbt_monitor = EM.add_periodic_timer(1) { manage_hrtbts }
|
115
|
-
end
|
116
|
-
|
122
|
+
#
|
123
|
+
# Kills the connection after sending a logout message, if applicable
|
124
|
+
#
|
117
125
|
def kill!
|
118
|
-
if @
|
119
|
-
log("Logging out
|
126
|
+
if @target_comp_id
|
127
|
+
log("Logging out #{peer}")
|
128
|
+
|
120
129
|
logout = FP::Messages::Logout.new
|
121
130
|
logout.text = 'Bye!'
|
131
|
+
|
122
132
|
send_msg(logout)
|
123
133
|
end
|
124
134
|
|
125
135
|
close_connection_after_writing
|
126
136
|
end
|
127
137
|
|
138
|
+
#
|
139
|
+
# Cleans up after we're done
|
140
|
+
#
|
128
141
|
def unbind
|
129
|
-
log("Terminating
|
130
|
-
|
131
|
-
@hrtbt_monitor && @hrtbt_monitor.cancel
|
142
|
+
log("Terminating connection to #{peer}")
|
143
|
+
@keep_alive_timer && @keep_alive_timer.cancel
|
132
144
|
end
|
133
145
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
146
|
+
#
|
147
|
+
# Notifies the connected peer it fucked up somehow and kill the connection
|
148
|
+
#
|
149
|
+
# @param error_msg [String] The reason to embed in the reject message
|
150
|
+
# @param msg_seq_num [Fixnum] The message sequence number this error pertains to
|
151
|
+
#
|
152
|
+
def peer_error(error_msg, msg_seq_num)
|
153
|
+
log("Notifying #{peer} of error: <#{error_msg}> and terminating")
|
154
|
+
|
155
|
+
rjct = FP::Messages::Reject.new
|
156
|
+
rjct.text = error_msg
|
157
|
+
rjct.ref_seq_num = msg_seq_num
|
158
|
+
|
140
159
|
send_msg(rjct)
|
141
160
|
kill!
|
142
161
|
end
|
143
162
|
|
144
|
-
|
163
|
+
#
|
164
|
+
# Maintains the message sequence consistency before handing off the message to +#handle_msg+
|
165
|
+
#
|
166
|
+
def process_msg(msg)
|
145
167
|
@recv_seq_num = msg.msg_seq_num
|
146
|
-
|
147
|
-
log("Received a <#{msg.class}> from <#{ip}:#{port}> with sequence number <#{msg.msg_seq_num}>")
|
148
168
|
|
149
|
-
|
150
|
-
if (@expected_clt_seq_num == @recv_seq_num)
|
169
|
+
log("Received a <#{msg.class}> from #{peer} with sequence number <#{msg.msg_seq_num}>")
|
151
170
|
|
171
|
+
# If sequence number == expected, then process it normally
|
172
|
+
if (@expected_seq_num == @recv_seq_num)
|
152
173
|
if @comp_id && msg.target_comp_id != @comp_id
|
153
|
-
@
|
174
|
+
@target_comp_id = msg.sender_comp_id
|
154
175
|
|
176
|
+
# Whoops, incorrect COMP_ID received, kill it with fire
|
155
177
|
if (msg.target_comp_id != @comp_id)
|
156
|
-
|
178
|
+
peer_error("Incorrect TARGET_COMP_ID in message, expected <#{@comp_id}>, got <#{msg.target_comp_id}>", msg.header.msg_seq_num)
|
157
179
|
end
|
158
180
|
|
159
181
|
else
|
160
|
-
if
|
161
|
-
log("Client authenticated as <#{msg.username}> with heartbeat interval of <#{msg.heart_bt_int}s> and message sequence number start <#{msg.msg_seq_num}>")
|
162
|
-
client.client_id = msg.username
|
163
|
-
@client_comp_id = msg.sender_comp_id
|
164
|
-
set_heartbeat_interval(msg.heart_bt_int)
|
165
|
-
|
166
|
-
logon = FP::Messages::Logon.new
|
167
|
-
logon.username = msg.username
|
168
|
-
logon.target_comp_id = msg.sender_comp_id
|
169
|
-
logon.sender_comp_id = msg.target_comp_id
|
170
|
-
logon.reset_seq_num_flag = true
|
171
|
-
send_msg(logon)
|
172
|
-
|
173
|
-
elsif @client_comp_id && msg.is_a?(FP::Messages::Logon)
|
174
|
-
log("Received second logon message, reset_seq_num_flag <#{msg.reset_seq_num_flag}>")
|
175
|
-
if msg.reset_seq_num_flag = 'Y'
|
176
|
-
@send_seq_num = 1
|
177
|
-
@messages = []
|
178
|
-
end
|
179
|
-
|
180
|
-
elsif !@client_comp_id
|
181
|
-
client_error("The session must be started with a logon message", msg.msg_seq_num, target_comp_id: msg.sender_comp_id)
|
182
|
-
|
183
|
-
elsif msg.is_a?(FP::Messages::Heartbeat)
|
182
|
+
if msg.is_a?(FP::Messages::Heartbeat)
|
184
183
|
# If we were expecting an answer to a test request we can sign it off and
|
185
184
|
# cancel the scheduled connection termination
|
186
185
|
if @pending_test_req_id && msg.test_req_id && (@pending_test_req_id == msg.test_req_id)
|
@@ -195,36 +194,40 @@ module Fix
|
|
195
194
|
|
196
195
|
elsif msg.is_a?(FP::Messages::ResendRequest)
|
197
196
|
# Re-send requested message range
|
198
|
-
@messages[msg.begin_seq_no, msg.end_seq_no.zero? ? @messages.length : msg.end_seq_no].each do |m|
|
197
|
+
@messages[msg.begin_seq_no - 1, (msg.end_seq_no.zero? ? @messages.length : (msg.end_seq_no - msg.begin_seq_no + 1))].each do |m|
|
199
198
|
log("Re-sending <#{m.class}> to <#{ip}:#{port}> with sequence number <#{m.msg_seq_num}>")
|
200
199
|
send_data(m.dump)
|
201
200
|
@last_send_at = Time.now.to_i
|
202
201
|
end
|
203
202
|
|
204
203
|
elsif msg.is_a?(FP::Message)
|
205
|
-
|
204
|
+
run_message_handler(msg)
|
206
205
|
end
|
207
206
|
end
|
208
207
|
|
209
|
-
@
|
210
|
-
|
211
|
-
elsif msg.is_a?(FP::Messages::Logon)
|
212
|
-
client_error("Expected logon message to have msg_seq_num = <1>, received <#{msg.msg_seq_num}>", msg.msg_seq_num, target_comp_id: msg.sender_comp_id)
|
208
|
+
@expected_seq_num += 1
|
213
209
|
|
214
|
-
elsif (@
|
215
|
-
log("Ignoring message <#{msg}> with stale sequence number <#{msg.msg_seq_num}>, expecting <#{@
|
210
|
+
elsif (@expected_seq_num > @recv_seq_num)
|
211
|
+
log("Ignoring message <#{msg}> with stale sequence number <#{msg.msg_seq_num}>, expecting <#{@expected_seq_num}>")
|
216
212
|
|
217
|
-
elsif (@
|
213
|
+
elsif (@expected_seq_num < @recv_seq_num) && @target_comp_id
|
218
214
|
# Request missing range when detect a gap
|
219
215
|
rr = FP::Messages::ResendRequest.new
|
220
|
-
rr.begin_seq_no = @
|
216
|
+
rr.begin_seq_no = @expected_seq_num
|
221
217
|
send_msg(rr)
|
222
218
|
end
|
223
219
|
|
224
220
|
self.last_request_at = Time.now.to_i
|
225
221
|
end
|
226
222
|
|
227
|
-
|
223
|
+
#
|
224
|
+
# Runs the defined message handler for the message's class
|
225
|
+
#
|
226
|
+
# @param msg [FP::Message] The message to handle
|
227
|
+
#
|
228
|
+
def run_message_handler(msg)
|
229
|
+
m = "on_#{msg.class.to_s.split('::').last.gsub(/(.)([A-Z])/, '\1_\2').downcase}".to_sym
|
230
|
+
send(m, msg) if respond_to?(m)
|
228
231
|
end
|
229
232
|
|
230
233
|
#
|
@@ -234,58 +237,27 @@ module Fix
|
|
234
237
|
# @param data [String] The received data chunk
|
235
238
|
#
|
236
239
|
def receive_data(data)
|
237
|
-
|
238
|
-
|
240
|
+
@buf ||= MessageBuffer.new do |parsed|
|
241
|
+
if (parsed.class == FP::ParseFailure) || !parsed.errors.count.zero?
|
242
|
+
peer_error("#{parsed.message} -- #{parsed.errors.join(", ")}", @expected_seq_num)
|
243
|
+
log("Failed to parse message <#{parsed.message}>")
|
244
|
+
parsed.errors.each { |err| log(" >>> #{err}") }
|
245
|
+
|
246
|
+
else
|
247
|
+
process_msg(parsed)
|
248
|
+
|
249
|
+
end
|
250
|
+
end
|
239
251
|
|
240
252
|
begin
|
241
|
-
|
253
|
+
@buf.add_data(data)
|
242
254
|
rescue
|
243
|
-
log("
|
255
|
+
log("Raised exception by #{peer} when parsing data <#{@buf.msg_buf.gsub(/\x01/, '|')}>, terminating.")
|
244
256
|
log($!.message + $!.backtrace.join("\n"))
|
245
257
|
kill!
|
246
258
|
end
|
247
259
|
end
|
248
260
|
|
249
|
-
#
|
250
|
-
# Attempts to parse fields from the message buffer, if the fields that get parsed
|
251
|
-
# complete the temporary message, it is handled
|
252
|
-
#
|
253
|
-
def parse_messages_from_buffer
|
254
|
-
while idx = msg_buf.index("\x01")
|
255
|
-
field = msg_buf.slice!(0, idx + 1).gsub(/\x01\Z/, '')
|
256
|
-
msg.append(field)
|
257
|
-
|
258
|
-
if msg.complete?
|
259
|
-
parsed = msg.parse!
|
260
|
-
if parsed.is_a?(FP::Message)
|
261
|
-
handle_msg(parsed)
|
262
|
-
elsif parsed.is_a?(FP::ParseFailure)
|
263
|
-
client_error(parsed.errors.join(", "), @expected_clt_seq_num, target_comp_id: (@client_comp_id || 'UNKNOWN'))
|
264
|
-
end
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
#
|
270
|
-
# The data buffer string
|
271
|
-
#
|
272
|
-
def msg_buf
|
273
|
-
@msg_buf ||= ''
|
274
|
-
end
|
275
|
-
|
276
|
-
#
|
277
|
-
# The temporary message to which fields get appended
|
278
|
-
#
|
279
|
-
def msg
|
280
|
-
@msg ||= MessageBuffer.new(@client)
|
281
|
-
end
|
282
|
-
|
283
|
-
def send_heartbeat(test_req_id = nil)
|
284
|
-
msg = FP::Messages::Heartbeat.new
|
285
|
-
test_req_id && msg.test_req_id = test_req_id
|
286
|
-
send_msg(msg)
|
287
|
-
end
|
288
|
-
|
289
261
|
end
|
290
262
|
end
|
291
263
|
end
|
data/lib/fix/engine/logger.rb
CHANGED
@@ -13,9 +13,21 @@ module Fix
|
|
13
13
|
|
14
14
|
attr_accessor :fields, :client
|
15
15
|
|
16
|
-
def initialize(
|
16
|
+
def initialize(&block)
|
17
17
|
@fields = []
|
18
|
-
|
18
|
+
|
19
|
+
raise "A block accepting a FP::Message as single parameter must be provided" unless (block && (block.arity == 1))
|
20
|
+
@msg_handler = block
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Adds received bytes to the message buffer and attempt to process them
|
25
|
+
#
|
26
|
+
# @param data [String] The received FIX message bits, as they come
|
27
|
+
#
|
28
|
+
def add_data(data)
|
29
|
+
msg_buf << data.chomp
|
30
|
+
parse_messages
|
19
31
|
end
|
20
32
|
|
21
33
|
#
|
@@ -41,42 +53,48 @@ module Fix
|
|
41
53
|
end
|
42
54
|
|
43
55
|
#
|
44
|
-
#
|
56
|
+
# Attempts to parse fields from the message buffer, if the fields that get parsed
|
57
|
+
# complete the temporary message, it is processed
|
45
58
|
#
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
59
|
+
def parse_messages(&block)
|
60
|
+
while idx = msg_buf.index("\x01")
|
61
|
+
field = msg_buf.slice!(0, idx + 1).gsub(/\x01\Z/, '')
|
62
|
+
append(field)
|
63
|
+
|
64
|
+
if complete?
|
65
|
+
parsed = FP.parse(to_s)
|
66
|
+
@fields = []
|
67
|
+
@msg_handler.call(parsed)
|
68
|
+
end
|
51
69
|
end
|
52
|
-
|
53
|
-
msg
|
54
70
|
end
|
55
71
|
|
56
72
|
#
|
57
|
-
#
|
58
|
-
# can start to get buffered right away
|
73
|
+
# The data buffer string
|
59
74
|
#
|
60
|
-
def
|
61
|
-
|
62
|
-
@fields = []
|
63
|
-
parsed
|
75
|
+
def msg_buf
|
76
|
+
@msg_buf ||= ''
|
64
77
|
end
|
65
78
|
|
79
|
+
#
|
80
|
+
# Returns a human-friendly string of the currently handled data
|
81
|
+
#
|
82
|
+
# @return [String] The parsed fields and the temporary buffer
|
83
|
+
#
|
66
84
|
def debug
|
67
|
-
to_s('|')
|
85
|
+
"#{to_s('|')}#{@msg_buf}"
|
68
86
|
end
|
69
87
|
|
88
|
+
#
|
89
|
+
# Returns the current fields as a string joined by a given separator
|
90
|
+
#
|
91
|
+
# @param sep [String] The separator
|
92
|
+
# @return [String] The fields joined by the separator
|
93
|
+
#
|
70
94
|
def to_s(sep = "\x01")
|
71
95
|
fields.map { |f| f.join('=') }.join(sep) + sep
|
72
96
|
end
|
73
97
|
|
74
|
-
def log_errors(msg)
|
75
|
-
log("Invalid message received <#{debug}>")
|
76
|
-
msg.errors.each { |e| log(" >>> #{e}") }
|
77
|
-
end
|
78
|
-
|
79
|
-
|
80
98
|
end
|
81
99
|
end
|
82
100
|
end
|
data/lib/fix/engine/server.rb
CHANGED
@@ -2,7 +2,7 @@ require 'eventmachine'
|
|
2
2
|
|
3
3
|
require 'fix/protocol'
|
4
4
|
require 'fix/engine/version'
|
5
|
-
require 'fix/engine/
|
5
|
+
require 'fix/engine/server_connection'
|
6
6
|
|
7
7
|
module Fix
|
8
8
|
module Engine
|
@@ -14,14 +14,18 @@ module Fix
|
|
14
14
|
|
15
15
|
include Logger
|
16
16
|
|
17
|
+
#
|
18
|
+
# Periodicity in seconds of logged status reports
|
19
|
+
#
|
17
20
|
REPORT_INTERVAL = 10
|
18
21
|
|
19
22
|
attr_accessor :ip, :port
|
20
23
|
|
21
|
-
def initialize(ip, port, handler)
|
24
|
+
def initialize(ip, port, handler, &block)
|
22
25
|
@ip = ip
|
23
26
|
@port = port
|
24
27
|
@handler = handler
|
28
|
+
@block = block
|
25
29
|
end
|
26
30
|
|
27
31
|
#
|
@@ -38,10 +42,15 @@ module Fix
|
|
38
42
|
#
|
39
43
|
def start_server
|
40
44
|
raise "EventMachine must be running to start a server" unless EM.reactor_running?
|
41
|
-
|
45
|
+
|
46
|
+
EM.start_server(ip, port, @handler) { |conn| @block && @block.call(conn) }
|
47
|
+
|
42
48
|
REPORT_INTERVAL && EM.add_periodic_timer(REPORT_INTERVAL) { report_status }
|
43
49
|
end
|
44
50
|
|
51
|
+
#
|
52
|
+
# Logs a short summary of the current server status
|
53
|
+
#
|
45
54
|
def report_status
|
46
55
|
log("#{Client.count} client(s) currently connected")
|
47
56
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'fix/engine/connection'
|
2
|
+
require 'fix/engine/client'
|
3
|
+
|
4
|
+
module Fix
|
5
|
+
module Engine
|
6
|
+
|
7
|
+
#
|
8
|
+
# The server connection wrapper, used when accepting a connection
|
9
|
+
#
|
10
|
+
module ServerConnection
|
11
|
+
|
12
|
+
include Connection
|
13
|
+
|
14
|
+
#
|
15
|
+
# Timespan during which a client must send a logon message after connecting
|
16
|
+
#
|
17
|
+
LOGON_TIMEOUT = 10
|
18
|
+
|
19
|
+
#
|
20
|
+
# Run after a client has connected
|
21
|
+
#
|
22
|
+
def post_init
|
23
|
+
super
|
24
|
+
|
25
|
+
@port, @ip = Socket.unpack_sockaddr_in(get_peername)
|
26
|
+
@client = Client.get(ip, port, self)
|
27
|
+
|
28
|
+
log("Client connected #{peer}, expecting logon message in the next #{LOGON_TIMEOUT}s")
|
29
|
+
|
30
|
+
EM.add_timer(LOGON_TIMEOUT) { logon_timeout }
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Logs the client out should he fail to authenticate before +LOGON_TIMEOUT+ seconds
|
35
|
+
#
|
36
|
+
def logon_timeout
|
37
|
+
unless @target_comp_id
|
38
|
+
log("Client #{peer} failed to authenticate before timeout, closing connection")
|
39
|
+
close_connection_after_writing
|
40
|
+
client.delete
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Returns the currently connected client
|
46
|
+
#
|
47
|
+
def client
|
48
|
+
Client.get(ip, port, self)
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# The way we refer to our connection peer in various logs and messages
|
53
|
+
#
|
54
|
+
def peer
|
55
|
+
"<#{client.key}>"
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Deletes the +FE::Client+ instance after the connection is terminated
|
60
|
+
#
|
61
|
+
def unbind
|
62
|
+
super
|
63
|
+
client.delete
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# We override +FE::Connection#run_message_handlers+ to add some session-related logic
|
68
|
+
#
|
69
|
+
def run_message_handler(msg)
|
70
|
+
if !@target_comp_id && msg.is_a?(FP::Messages::Logon)
|
71
|
+
log("Peer authenticated as <#{msg.username}> with heartbeat interval of <#{msg.heart_bt_int}s> and message sequence number start <#{msg.msg_seq_num}>")
|
72
|
+
client.username = msg.username
|
73
|
+
@target_comp_id = msg.sender_comp_id
|
74
|
+
set_heartbeat_interval(msg.heart_bt_int)
|
75
|
+
|
76
|
+
logon = FP::Messages::Logon.new
|
77
|
+
logon.username = msg.username
|
78
|
+
logon.target_comp_id = msg.sender_comp_id
|
79
|
+
logon.sender_comp_id = msg.target_comp_id
|
80
|
+
logon.reset_seq_num_flag = true
|
81
|
+
|
82
|
+
send_msg(logon)
|
83
|
+
|
84
|
+
elsif @target_comp_id && msg.is_a?(FP::Messages::Logon)
|
85
|
+
log("Received second logon message, reset_seq_num_flag <#{msg.reset_seq_num_flag}>")
|
86
|
+
if msg.reset_seq_num_flag = 'Y'
|
87
|
+
@send_seq_num = 1
|
88
|
+
@messages = []
|
89
|
+
end
|
90
|
+
|
91
|
+
elsif !@target_comp_id
|
92
|
+
peer_error("The session must be started with a logon message", msg.msg_seq_num, target_comp_id: msg.sender_comp_id)
|
93
|
+
|
94
|
+
else
|
95
|
+
super(msg)
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
data/lib/fix/engine/version.rb
CHANGED
metadata
CHANGED
@@ -1,128 +1,120 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fix-engine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David François
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-01-
|
11
|
+
date: 2015-01-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ~>
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '3.1'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ~>
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '3.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ~>
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '10.3'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ~>
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '10.3'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: yard
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ~>
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
47
|
+
version: '0.8'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ~>
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: pry
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - '>='
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - '>='
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
54
|
+
version: '0.8'
|
69
55
|
- !ruby/object:Gem::Dependency
|
70
56
|
name: redcarpet
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
72
58
|
requirements:
|
73
|
-
- -
|
59
|
+
- - ~>
|
74
60
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
61
|
+
version: '3.1'
|
76
62
|
type: :development
|
77
63
|
prerelease: false
|
78
64
|
version_requirements: !ruby/object:Gem::Requirement
|
79
65
|
requirements:
|
80
|
-
- -
|
66
|
+
- - ~>
|
81
67
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
68
|
+
version: '3.1'
|
83
69
|
- !ruby/object:Gem::Dependency
|
84
70
|
name: simplecov
|
85
71
|
requirement: !ruby/object:Gem::Requirement
|
86
72
|
requirements:
|
87
|
-
- -
|
73
|
+
- - ~>
|
88
74
|
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
75
|
+
version: '0.9'
|
90
76
|
type: :development
|
91
77
|
prerelease: false
|
92
78
|
version_requirements: !ruby/object:Gem::Requirement
|
93
79
|
requirements:
|
94
|
-
- -
|
80
|
+
- - ~>
|
95
81
|
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
82
|
+
version: '0.9'
|
97
83
|
- !ruby/object:Gem::Dependency
|
98
84
|
name: fix-protocol
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
100
86
|
requirements:
|
101
|
-
- -
|
87
|
+
- - ~>
|
102
88
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
89
|
+
version: 0.0.64
|
104
90
|
type: :runtime
|
105
91
|
prerelease: false
|
106
92
|
version_requirements: !ruby/object:Gem::Requirement
|
107
93
|
requirements:
|
108
|
-
- -
|
94
|
+
- - ~>
|
109
95
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
96
|
+
version: 0.0.64
|
111
97
|
- !ruby/object:Gem::Dependency
|
112
98
|
name: eventmachine
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
114
100
|
requirements:
|
115
|
-
- -
|
101
|
+
- - ~>
|
116
102
|
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
103
|
+
version: '1.0'
|
118
104
|
type: :runtime
|
119
105
|
prerelease: false
|
120
106
|
version_requirements: !ruby/object:Gem::Requirement
|
121
107
|
requirements:
|
122
|
-
- -
|
108
|
+
- - ~>
|
123
109
|
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
|
-
description: FIX engine
|
110
|
+
version: '1.0'
|
111
|
+
description: " The FIX engine library allows one to easily connect to a FIX acceptor
|
112
|
+
and establish\n a session, it will handle the administrative messages such as logons,
|
113
|
+
hearbeats, gap fills and\n allow custom handling of business level messages.\n\n
|
114
|
+
\ Likewise, an acceptor may be easily implemented by defining callbacks for business
|
115
|
+
level messages.\n\n FIX protocol message parsing capabilities are provided by the
|
116
|
+
fix-protocol gem, which\n currently supports the administrative subset (and a few
|
117
|
+
business level messages) of the FIX 4.4\n message specification. \n"
|
126
118
|
email:
|
127
119
|
- david.francois@paymium.com
|
128
120
|
executables: []
|
@@ -134,13 +126,16 @@ files:
|
|
134
126
|
- bin/fix-engine
|
135
127
|
- lib/fix/engine.rb
|
136
128
|
- lib/fix/engine/client.rb
|
129
|
+
- lib/fix/engine/client_connection.rb
|
137
130
|
- lib/fix/engine/connection.rb
|
138
131
|
- lib/fix/engine/logger.rb
|
139
132
|
- lib/fix/engine/message_buffer.rb
|
140
133
|
- lib/fix/engine/server.rb
|
134
|
+
- lib/fix/engine/server_connection.rb
|
141
135
|
- lib/fix/engine/version.rb
|
142
136
|
homepage: https://github.com/paymium/fix-engine
|
143
|
-
licenses:
|
137
|
+
licenses:
|
138
|
+
- MIT
|
144
139
|
metadata: {}
|
145
140
|
post_install_message:
|
146
141
|
rdoc_options: []
|