@@ -21,18 +21,18 @@ class SwooleIO extends AbstractIO
21
21
* @param int $port
22
22
* @param float $connection_timeout
23
23
* @param float $read_write_timeout
24
- * @param null $context
24
+ * @param mixed $context
25
25
* @param bool $keepalive
26
26
* @param int $heartbeat
27
27
*/
28
28
public function __construct (
29
- $ host ,
30
- $ port ,
31
- $ connection_timeout ,
32
- $ read_write_timeout ,
33
- $ context = null ,
34
- $ keepalive = false ,
35
- $ heartbeat = 0
29
+ string $ host ,
30
+ int $ port ,
31
+ float $ connection_timeout ,
32
+ float $ read_write_timeout ,
33
+ mixed $ context = null ,
34
+ bool $ keepalive = false ,
35
+ int $ heartbeat = 0
36
36
) {
37
37
/*
38
38
TODO FUTURE enable this check
@@ -58,8 +58,11 @@ public function __construct(
58
58
* @throws AMQPRuntimeException
59
59
* @throws AMQPIOException
60
60
*/
61
- public function connect ()
61
+ public function connect (): void
62
62
{
63
+ // Close any existing connection to prevent resource leaks
64
+ $ this ->close ();
65
+
63
66
$ this ->sock = new Client (SWOOLE_SOCK_TCP );
64
67
65
68
// Set socket options before connecting
@@ -71,9 +74,12 @@ public function connect()
71
74
'open_tcp_nodelay ' => true ,
72
75
'tcp_keepalive ' => $ this ->keepalive ,
73
76
'package_max_length ' => 2 * 1024 * 1024 , // 2MB max package
77
+ 'socket_buffer_size ' => 2 * 1024 * 1024 ,
78
+ 'buffer_output_size ' => 2 * 1024 * 1024 ,
74
79
]);
75
80
76
81
if (!$ this ->sock ->connect ($ this ->host , $ this ->port )) {
82
+ $ this ->close ();
77
83
throw new AMQPIOException (
78
84
sprintf (
79
85
'Error Connecting to server(%s): %s ' ,
@@ -93,7 +99,7 @@ public function connect()
93
99
* @throws AMQPTimeoutException
94
100
* @throws AMQPConnectionClosedException
95
101
*/
96
- public function read ($ len )
102
+ public function read ($ len ): string
97
103
{
98
104
if ($ this ->sock === null ) {
99
105
throw new AMQPConnectionClosedException ('Socket connection is closed ' );
@@ -115,27 +121,28 @@ public function read($len)
115
121
// Read remaining bytes from socket
116
122
while ($ remaining > 0 ) {
117
123
if (!$ this ->sock ->connected ) {
124
+ $ this ->close ();
118
125
throw new AMQPConnectionClosedException ('Broken pipe or closed connection ' );
119
126
}
120
127
121
128
// Swoole recv() returns false on error, empty string on EOF
122
129
$ chunk = $ this ->sock ->recv ($ remaining , $ this ->read_timeout );
123
130
124
- if ($ chunk === false ) {
131
+ if ($ chunk === '' || $ chunk === false ) {
132
+ $ this ->close ();
125
133
if ($ this ->sock ->errCode == SOCKET_ETIMEDOUT ) {
126
134
throw new AMQPTimeoutException ('Read timeout ' );
127
135
}
128
- throw new AMQPIOException (
129
- sprintf ('Error receiving data: %s ' , swoole_strerror ($ this ->sock ->errCode )),
130
- $ this ->sock ->errCode
131
- );
132
- }
133
-
134
- if ($ chunk === '' ) {
136
+ if ($ this ->sock ->errCode !== 0 ) {
137
+ throw new AMQPIOException (
138
+ sprintf ('Error receiving data: %s ' , swoole_strerror ($ this ->sock ->errCode )),
139
+ $ this ->sock ->errCode
140
+ );
141
+ }
135
142
throw new AMQPConnectionClosedException ('Connection closed by peer ' );
136
143
}
137
144
138
- $ data .= $ chunk ;
145
+ $ data .= $ chunk ;
139
146
$ remaining -= strlen ($ chunk );
140
147
}
141
148
@@ -148,30 +155,44 @@ public function read($len)
148
155
* @throws AMQPRuntimeException
149
156
* @throws AMQPTimeoutException
150
157
* @throws AMQPConnectionClosedException
158
+ * @throws AMQPIOException
151
159
*/
152
- public function write ($ data )
160
+ public function write ($ data ): void
153
161
{
154
162
if ($ this ->sock === null || !$ this ->sock ->connected ) {
163
+ $ this ->close ();
155
164
throw new AMQPConnectionClosedException ('Socket connection is closed ' );
156
165
}
157
166
158
167
$ this ->checkBrokerHeartbeat ();
159
168
160
- // Swoole send() handles partial writes internally
161
- $ result = $ this -> sock -> send ( $ data ) ;
169
+ $ totalLength = strlen ( $ data );
170
+ $ offset = 0 ;
162
171
163
- if ($ result === false ) {
164
- if ($ this ->sock ->errCode == SOCKET_ETIMEDOUT ) {
165
- throw new AMQPTimeoutException ('Write timeout ' );
172
+ while ($ offset < $ totalLength ) {
173
+ // Send remaining bytes; avoid extra substr() call for the first chunk
174
+ $ chunk = $ offset === 0 ? $ data : substr ($ data , $ offset );
175
+
176
+ $ sent = $ this ->sock ->send ($ chunk );
177
+
178
+ if ($ sent === false ) {
179
+ $ this ->close ();
180
+ if ($ this ->sock ->errCode == SOCKET_ETIMEDOUT ) {
181
+ throw new AMQPTimeoutException ('Write timeout ' );
182
+ }
183
+ throw new AMQPIOException (
184
+ sprintf ('Error sending data: %s ' , swoole_strerror ($ this ->sock ->errCode )),
185
+ $ this ->sock ->errCode
186
+ );
166
187
}
167
- throw new AMQPIOException (
168
- sprintf ('Error sending data: %s ' , swoole_strerror ($ this ->sock ->errCode )),
169
- $ this ->sock ->errCode
170
- );
171
- }
172
188
173
- if ($ result !== strlen ($ data )) {
174
- throw new AMQPIOException ('Could not write entire buffer ' );
189
+ if ($ sent === 0 ) {
190
+ // Peer closed the connection
191
+ $ this ->close ();
192
+ throw new AMQPConnectionClosedException ('Connection closed by peer while writing ' );
193
+ }
194
+
195
+ $ offset += $ sent ;
175
196
}
176
197
177
198
$ this ->last_write = microtime (true );
@@ -180,7 +201,7 @@ public function write($data)
180
201
/**
181
202
* @return void
182
203
*/
183
- public function close ()
204
+ public function close (): void
184
205
{
185
206
if ($ this ->sock !== null && $ this ->sock ->connected ) {
186
207
$ this ->sock ->close ();
@@ -191,13 +212,21 @@ public function close()
191
212
$ this ->buffer = '' ;
192
213
}
193
214
215
+ /**
216
+ * Ensure the socket is closed when the object is destroyed.
217
+ */
218
+ public function __destruct ()
219
+ {
220
+ $ this ->close ();
221
+ }
222
+
194
223
/**
195
224
* @param int|null $sec
196
225
* @param int $usec
197
226
* @return int|bool
198
227
* @throws AMQPConnectionClosedException
199
228
*/
200
- protected function do_select (?int $ sec , int $ usec )
229
+ protected function do_select (?int $ sec , int $ usec ): bool | int
201
230
{
202
231
if ($ this ->sock === null || !$ this ->sock ->connected ) {
203
232
throw new AMQPConnectionClosedException ('Socket connection is closed ' );
@@ -222,12 +251,14 @@ protected function do_select(?int $sec, int $usec)
222
251
}
223
252
// Connection error
224
253
if ($ this ->sock ->errCode == SOCKET_ECONNRESET || !$ this ->sock ->connected ) {
254
+ $ this ->close ();
225
255
throw new AMQPConnectionClosedException ('Connection reset by peer ' );
226
256
}
227
257
return false ; // Other error
228
258
}
229
259
230
260
if ($ data === '' ) {
261
+ $ this ->close ();
231
262
throw new AMQPConnectionClosedException ('Connection closed by peer ' );
232
263
}
233
264
@@ -239,7 +270,7 @@ protected function do_select(?int $sec, int $usec)
239
270
/**
240
271
* @return Client|null
241
272
*/
242
- public function getSocket ()
273
+ public function getSocket (): ? Client
243
274
{
244
275
return $ this ->sock ;
245
276
}
0 commit comments