Browse Source

Relatively large update to TCP API. No more "protocol".

Instead servers are passed a function which gets called on connection (like
in the original design) which has one argument, the connecting socket. The
user sets up callbacks on that. It's pretty much how I had it originally.

Encoding is now set via v8 getter/setter and can be changed dynamically.

The timeout for all sockets is fixed at 60 seconds for now. Need to fix
that.
v0.7.4-release
Ryan 16 years ago
parent
commit
73fb24f48d
  1. 66
      node.html
  2. 18
      src/http.cc
  3. 1
      src/http.h
  4. 126
      src/net.cc
  5. 8
      src/net.h
  6. 44
      test/test-pingpong.js

66
node.html

@ -30,7 +30,7 @@ body {
} }
#toc a { color: #777; } #toc a { color: #777; }
h1, h2, h3 { color: #aaf; } h1, h2, h3, h4 { color: #bbb; }
h1 { h1 {
margin: 2em 0; margin: 2em 0;
@ -42,22 +42,30 @@ h1 {
h1 a { color: inherit; } h1 a { color: inherit; }
h2 { h2 {
font-size: 45px; font-size: 30px;
line-height: inherit; line-height: inherit;
font-weight: bold; font-weight: bold;
margin: 2em 0;
} }
h3 { h3 {
margin: 2em 0;
font-size: 20px;
line-height: inherit;
font-weight: bold;
}
h4 {
margin: 1em 0; margin: 1em 0;
font-size: 30px; font-size: inherit;
line-height: inherit; line-height: inherit;
font-weight: inherit; font-weight: bold;
} }
pre, code { pre, code {
font-family: monospace; font-family: monospace;
font-size: 13pt; font-size: 13pt;
color: #eae; color: #aaf;
} }
pre { pre {
@ -137,31 +145,33 @@ Check out <a href="#api">the documentation</a> for more examples.
<h2 id="motivation">Motivation</h2> <h2 id="motivation">Motivation</h2>
<ol> <h3>Evented Programming Makes More Sense</h3>
<li>Evented programming makes sense
<ol> difference between blocking/non-blocking design
<li>difference between blocking/non-blocking design
<p> There are many methods to write internet servers but they can <p> There are many methods to write internet servers but they can
fundamentally be divided into two camps: evented and threaded; non-blocking fundamentally be divided into two camps: evented and threaded; non-blocking
and blocking. A blocking server accepts a connection and launches a new and blocking. A blocking server accepts a connection and launches a new
thread to handle the connection. Because the concurrency is handled by thread to handle the connection. Because the concurrency is handled by
the thread scheduler, a blocking server can make function calls which the thread scheduler, a blocking server can make function calls which
preform full network requests. preform full network requests.
<pre class="sh_javascript">var response = db.execute("SELECT * FROM table"); <pre class="sh_javascript">var response = db.execute("SELECT * FROM table");
// do something</pre> // do something</pre>
<p> An evented server manages its concurrency itself. All connections <p> An evented server manages its concurrency itself. All connections
are handled in a single thread and callbacks are executed on certain are handled in a single thread and callbacks are executed on certain
events: "socket 23 is has data to read", "socket 65's write buffer is events: "socket 23 is has data to read", "socket 65's write buffer is
empty". An evented server executes small bits of code but never empty". An evented server executes small bits of code but never
<i>blocks</i> the process. In the evented world callbacks are used <i>blocks</i> the process. In the evented world callbacks are used
instead of functions instead of functions
<pre class="sh_javascript">db.execute("SELECT * FROM table", function (response) { <pre class="sh_javascript">db.execute("SELECT * FROM table", function (response) {
// do something // do something
});</pre> });</pre>
<li><a href="http://duartes.org/gustavo/blog/post/what-your-computer-does-while-you-wait">I/O latency</a>
<p><a href="http://duartes.org/gustavo/blog/post/what-your-computer-does-while-you-wait">I/O latency</a>
<pre> <pre>
l1 cache ~ 3 l1 cache ~ 3
l2 cache ~ 14 l2 cache ~ 14
@ -169,9 +179,10 @@ l2 cache ~ 14
disk ~ 41000000 disk ~ 41000000
network ~ 240000000 network ~ 240000000
</pre> </pre>
<li>purely evented interfaces rule out a lot of stupidity
</ol> <p>purely evented interfaces rule out a lot of stupidity
<li>Evented programs are more efficient
<h3>Evented programs are more efficient</h3>
<ol> <ol>
<li>pthread stack size <li>pthread stack size
2mb default stack size on linux (1mb on windows, 64kb on FreeBSD) 2mb default stack size on linux (1mb on windows, 64kb on FreeBSD)
@ -180,7 +191,7 @@ l2 cache ~ 14
<li>Apache vs. Nginx <li>Apache vs. Nginx
<li>event machine vs mongrel (neverblock) <li>event machine vs mongrel (neverblock)
</ol> </ol>
<li>The appropriateness of Javascript <h3>The appropriateness of Javascript</h3>
<ol> <ol>
<li>No I/O <li>No I/O
<p> Javascript is without I/O. In the browser the DOM provides I/O, <p> Javascript is without I/O. In the browser the DOM provides I/O,
@ -200,7 +211,6 @@ l2 cache ~ 14
systems tend to be written in C and portable web-level systems are systems tend to be written in C and portable web-level systems are
written in Javascript. written in Javascript.
</ol> </ol>
</ol>
<h2 id="benchmarks">Benchmarks</h2> <h2 id="benchmarks">Benchmarks</h2>

18
src/http.cc

@ -60,7 +60,7 @@ HTTPConnection::v8NewClient (const Arguments& args)
if (args[0]->IsFunction() == false) if (args[0]->IsFunction() == false)
return ThrowException(String::New("Must pass a class as the first argument.")); return ThrowException(String::New("Must pass a class as the first argument."));
Local<Function> protocol_class = Local<Function>::Cast(args[0]); Local<Function> protocol_class = Local<Function>::Cast(args[0]);
new HTTPConnection(args.This(), protocol_class, HTTP_RESPONSE); new HTTPConnection(args.This(), HTTP_RESPONSE);
return args.This(); return args.This();
} }
@ -71,7 +71,7 @@ HTTPConnection::v8NewServer (const Arguments& args)
if (args[0]->IsFunction() == false) if (args[0]->IsFunction() == false)
return ThrowException(String::New("Must pass a class as the first argument.")); return ThrowException(String::New("Must pass a class as the first argument."));
Local<Function> protocol_class = Local<Function>::Cast(args[0]); Local<Function> protocol_class = Local<Function>::Cast(args[0]);
new HTTPConnection(args.This(), protocol_class, HTTP_REQUEST); new HTTPConnection(args.This(), HTTP_REQUEST);
return args.This(); return args.This();
} }
@ -90,8 +90,7 @@ HTTPConnection::on_message_begin (http_parser *parser)
HTTPConnection *connection = static_cast<HTTPConnection*> (parser->data); HTTPConnection *connection = static_cast<HTTPConnection*> (parser->data);
HandleScope scope; HandleScope scope;
Local<Object> protocol = connection->GetProtocol(); Local<Value> on_message_v = connection->handle_->Get(ON_MESSAGE_SYMBOL);
Local<Value> on_message_v = protocol->Get(ON_MESSAGE_SYMBOL);
if (!on_message_v->IsFunction()) return -1; if (!on_message_v->IsFunction()) return -1;
Handle<Function> on_message = Handle<Function>::Cast(on_message_v); Handle<Function> on_message = Handle<Function>::Cast(on_message_v);
@ -272,8 +271,8 @@ HTTPConnection::on_message_complete (http_parser *parser)
return 0; return 0;
} }
HTTPConnection::HTTPConnection (Handle<Object> handle, Handle<Function> protocol_class, enum http_parser_type type) HTTPConnection::HTTPConnection (Handle<Object> handle, enum http_parser_type type)
: Connection(handle, protocol_class) : Connection(handle)
{ {
http_parser_init (&parser_, type); http_parser_init (&parser_, type);
parser_.on_message_begin = on_message_begin; parser_.on_message_begin = on_message_begin;
@ -330,15 +329,14 @@ HTTPServer::OnConnection (struct sockaddr *addr, socklen_t len)
{ {
HandleScope scope; HandleScope scope;
Local<Function> protocol_class = GetProtocolClass(); Local<Function> connection_handler = GetConnectionHandler ();
if (protocol_class.IsEmpty()) { if (connection_handler.IsEmpty()) {
Close(); Close();
return NULL; return NULL;
} }
Handle<Value> argv[] = { protocol_class };
Local<Object> connection_handle = Local<Object> connection_handle =
HTTPConnection::server_constructor_template->GetFunction()->NewInstance(1, argv); HTTPConnection::server_constructor_template->GetFunction()->NewInstance(0, NULL);
HTTPConnection *connection = NODE_UNWRAP(HTTPConnection, connection_handle); HTTPConnection *connection = NODE_UNWRAP(HTTPConnection, connection_handle);
connection->SetAcceptor(handle_); connection->SetAcceptor(handle_);

1
src/http.h

@ -19,7 +19,6 @@ protected:
static v8::Handle<v8::Value> v8NewServer (const v8::Arguments& args); static v8::Handle<v8::Value> v8NewServer (const v8::Arguments& args);
HTTPConnection (v8::Handle<v8::Object> handle, HTTPConnection (v8::Handle<v8::Object> handle,
v8::Handle<v8::Function> protocol_class,
enum http_parser_type type); enum http_parser_type type);
void OnReceive (const void *buf, size_t len); void OnReceive (const void *buf, size_t len);

126
src/net.cc

@ -13,6 +13,9 @@
using namespace v8; using namespace v8;
using namespace node; using namespace node;
#define UTF8_SYMBOL String::NewSymbol("utf8")
#define RAW_SYMBOL String::NewSymbol("raw")
#define ON_RECEIVE_SYMBOL String::NewSymbol("onReceive") #define ON_RECEIVE_SYMBOL String::NewSymbol("onReceive")
#define ON_DISCONNECT_SYMBOL String::NewSymbol("onDisconnect") #define ON_DISCONNECT_SYMBOL String::NewSymbol("onDisconnect")
#define ON_CONNECT_SYMBOL String::NewSymbol("onConnect") #define ON_CONNECT_SYMBOL String::NewSymbol("onConnect")
@ -26,7 +29,7 @@ using namespace node;
#define SERVER_SYMBOL String::NewSymbol("server") #define SERVER_SYMBOL String::NewSymbol("server")
#define PROTOCOL_SYMBOL String::NewSymbol("protocol") #define PROTOCOL_SYMBOL String::NewSymbol("protocol")
#define PROTOCOL_CLASS_SYMBOL String::NewSymbol("protocol_class") #define CONNECTION_HANDLER_SYMBOL String::NewSymbol("connection_handler")
static const struct addrinfo tcp_hints = static const struct addrinfo tcp_hints =
/* ai_flags */ { AI_PASSIVE /* ai_flags */ { AI_PASSIVE
@ -57,36 +60,50 @@ Connection::Initialize (v8::Handle<v8::Object> target)
NODE_SET_PROTOTYPE_METHOD(constructor_template, "fullClose", v8FullClose); NODE_SET_PROTOTYPE_METHOD(constructor_template, "fullClose", v8FullClose);
NODE_SET_PROTOTYPE_METHOD(constructor_template, "forceClose", v8ForceClose); NODE_SET_PROTOTYPE_METHOD(constructor_template, "forceClose", v8ForceClose);
constructor_template->PrototypeTemplate()->SetAccessor(
String::NewSymbol("encoding"),
EncodingGetter,
EncodingSetter);
target->Set(String::NewSymbol("Connection"), constructor_template->GetFunction()); target->Set(String::NewSymbol("Connection"), constructor_template->GetFunction());
} }
Connection::Connection (Handle<Object> handle, Handle<Function> protocol_class) Handle<Value>
: ObjectWrap(handle) Connection::EncodingGetter (Local<String> _, const AccessorInfo& info)
{ {
Connection *connection = NODE_UNWRAP(Connection, info.This());
HandleScope scope; HandleScope scope;
// Instanciate the protocol object if (connection->encoding_ == UTF8)
Handle<Value> argv[] = { handle_ }; return scope.Close(UTF8_SYMBOL);
Local<Object> protocol = protocol_class->NewInstance(1, argv); else
handle_->Set(PROTOCOL_SYMBOL, protocol); return scope.Close(RAW_SYMBOL);
}
// TODO use SetNamedPropertyHandler (or whatever) for encoding and timeout
// instead of just reading it once?
encoding_ = RAW; void
Local<Value> encoding_v = protocol->Get(ENCODING_SYMBOL); Connection::EncodingSetter (Local<String> _, Local<Value> value, const AccessorInfo& info)
if (encoding_v->IsString()) { {
Local<String> encoding_string = encoding_v->ToString(); Connection *connection = NODE_UNWRAP(Connection, info.This());
char buf[5]; // need enough room for "utf8" or "raw" if (!value->IsString()) {
encoding_string->WriteAscii(buf, 0, 4); connection->encoding_ = RAW;
buf[4] = '\0'; return;
if(strcasecmp(buf, "utf8") == 0) encoding_ = UTF8;
} }
HandleScope scope;
Local<String> encoding = value->ToString();
char buf[5]; // need enough room for "utf8" or "raw"
encoding->WriteAscii(buf, 0, 4);
buf[4] = '\0';
if(strcasecmp(buf, "utf8") == 0)
connection->encoding_ = UTF8;
else
connection->encoding_ = RAW;
}
double timeout = 0.0; // default Connection::Connection (Handle<Object> handle)
Local<Value> timeout_v = protocol->Get(TIMEOUT_SYMBOL); : ObjectWrap(handle)
if (encoding_v->IsInt32()) {
timeout = timeout_v->Int32Value() / 1000.0; encoding_ = RAW;
double timeout = 60.0; // default
host_ = NULL; host_ = NULL;
port_ = NULL; port_ = NULL;
@ -111,20 +128,6 @@ Connection::~Connection ()
ForceClose(); ForceClose();
} }
Local<Object>
Connection::GetProtocol (void)
{
HandleScope scope;
Local<Value> protocol_v = handle_->Get(PROTOCOL_SYMBOL);
if (protocol_v->IsObject()) {
Local<Object> protocol = protocol_v->ToObject();
return scope.Close(protocol);
}
return Local<Object>();
}
void void
Connection::SetAcceptor (Handle<Object> acceptor_handle) Connection::SetAcceptor (Handle<Object> acceptor_handle)
{ {
@ -138,10 +141,7 @@ Handle<Value>
Connection::v8New (const Arguments& args) Connection::v8New (const Arguments& args)
{ {
HandleScope scope; HandleScope scope;
if (args[0]->IsFunction() == false) new Connection(args.This());
return ThrowException(String::New("Must pass a class as the first argument."));
Handle<Function> protocol_class = Handle<Function>::Cast(args[0]);
new Connection(args.This(), protocol_class);
return args.This(); return args.This();
} }
@ -290,8 +290,7 @@ Connection::OnReceive (const void *buf, size_t len)
{ {
HandleScope scope; HandleScope scope;
Local<Object> protocol = GetProtocol(); Handle<Value> callback_v = handle_->Get(ON_RECEIVE_SYMBOL);
Handle<Value> callback_v = protocol->Get(ON_RECEIVE_SYMBOL);
if (!callback_v->IsFunction()) return; if (!callback_v->IsFunction()) return;
Handle<Function> callback = Handle<Function>::Cast(callback_v); Handle<Function> callback = Handle<Function>::Cast(callback_v);
@ -319,7 +318,7 @@ Connection::OnReceive (const void *buf, size_t len)
} }
TryCatch try_catch; TryCatch try_catch;
callback->Call(protocol, argc, argv); callback->Call(handle_, argc, argv);
if (try_catch.HasCaught()) if (try_catch.HasCaught())
fatal_exception(try_catch); // XXX is this the right action to take? fatal_exception(try_catch); // XXX is this the right action to take?
@ -329,11 +328,10 @@ Connection::OnReceive (const void *buf, size_t len)
void name () \ void name () \
{ \ { \
HandleScope scope; \ HandleScope scope; \
Local<Object> protocol = GetProtocol(); \ Local<Value> callback_v = handle_->Get(symbol); \
Local<Value> callback_v = protocol->Get(symbol); \
if (!callback_v->IsFunction()) return; \ if (!callback_v->IsFunction()) return; \
Handle<Function> callback = Handle<Function>::Cast(callback_v); \ Handle<Function> callback = Handle<Function>::Cast(callback_v); \
callback->Call(protocol, 0, NULL); \ callback->Call(handle_, 0, NULL); \
} }
DEFINE_SIMPLE_CALLBACK(Connection::OnConnect, ON_CONNECT_SYMBOL) DEFINE_SIMPLE_CALLBACK(Connection::OnConnect, ON_CONNECT_SYMBOL)
@ -360,12 +358,12 @@ Acceptor::Initialize (Handle<Object> target)
target->Set(String::NewSymbol("Server"), constructor_template->GetFunction()); target->Set(String::NewSymbol("Server"), constructor_template->GetFunction());
} }
Acceptor::Acceptor (Handle<Object> handle, Handle<Function> protocol_class, Handle<Object> options) Acceptor::Acceptor (Handle<Object> handle, Handle<Function> connection_handler, Handle<Object> options)
: ObjectWrap(handle) : ObjectWrap(handle)
{ {
HandleScope scope; HandleScope scope;
handle_->SetHiddenValue(PROTOCOL_CLASS_SYMBOL, protocol_class); handle_->SetHiddenValue(CONNECTION_HANDLER_SYMBOL, connection_handler);
int backlog = 1024; // default value int backlog = 1024; // default value
Local<Value> backlog_v = options->Get(String::NewSymbol("backlog")); Local<Value> backlog_v = options->Get(String::NewSymbol("backlog"));
@ -388,20 +386,27 @@ Acceptor::OnConnection (struct sockaddr *addr, socklen_t len)
{ {
HandleScope scope; HandleScope scope;
Local<Function> protocol_class = GetProtocolClass(); Local<Function> connection_handler = GetConnectionHandler();
if (protocol_class.IsEmpty()) { if (connection_handler.IsEmpty()) {
printf("protocol class was empty!"); printf("Connection handler was empty!");
Close(); Close();
return NULL; return NULL;
} }
Handle<Value> argv[] = { protocol_class };
Local<Object> connection_handle = Local<Object> connection_handle =
Connection::constructor_template->GetFunction()->NewInstance(1, argv); Connection::constructor_template->GetFunction()->NewInstance(0, NULL);
Connection *connection = NODE_UNWRAP(Connection, connection_handle); Connection *connection = NODE_UNWRAP(Connection, connection_handle);
connection->SetAcceptor(handle_); connection->SetAcceptor(handle_);
Handle<Value> argv[1] = { connection_handle };
TryCatch try_catch;
Local<Value> ret = connection_handler->Call(handle_, 1, argv);
if (ret.IsEmpty())
fatal_exception(try_catch);
return connection; return connection;
} }
@ -413,7 +418,7 @@ Acceptor::v8New (const Arguments& args)
if (args.Length() < 1 || args[0]->IsFunction() == false) if (args.Length() < 1 || args[0]->IsFunction() == false)
return ThrowException(String::New("Must at give connection handler as the first argument")); return ThrowException(String::New("Must at give connection handler as the first argument"));
Local<Function> protocol_class = Local<Function>::Cast(args[0]); Local<Function> connection_handler = Local<Function>::Cast(args[0]);
Local<Object> options; Local<Object> options;
if (args.Length() > 1 && args[1]->IsObject()) { if (args.Length() > 1 && args[1]->IsObject()) {
@ -422,7 +427,7 @@ Acceptor::v8New (const Arguments& args)
options = Object::New(); options = Object::New();
} }
new Acceptor(args.This(), protocol_class, options); new Acceptor(args.This(), connection_handler, options);
return args.This(); return args.This();
} }
@ -468,15 +473,16 @@ Acceptor::v8Close (const Arguments& args)
} }
Local<v8::Function> Local<v8::Function>
Acceptor::GetProtocolClass (void) Acceptor::GetConnectionHandler (void)
{ {
HandleScope scope; HandleScope scope;
Local<Value> protocol_class_v = handle_->GetHiddenValue(PROTOCOL_CLASS_SYMBOL); Local<Value> connection_handler_v = handle_->GetHiddenValue(CONNECTION_HANDLER_SYMBOL);
if (protocol_class_v->IsFunction()) { if (connection_handler_v->IsFunction()) {
Local<Function> protocol_class = Local<Function>::Cast(protocol_class_v); Local<Function> connection_handler = Local<Function>::Cast(connection_handler_v);
return scope.Close(protocol_class); return scope.Close(connection_handler);
} }
return Local<Function>(); return Local<Function>();
} }

8
src/net.h

@ -22,8 +22,10 @@ protected:
static v8::Handle<v8::Value> v8Close (const v8::Arguments& args); static v8::Handle<v8::Value> v8Close (const v8::Arguments& args);
static v8::Handle<v8::Value> v8FullClose (const v8::Arguments& args); static v8::Handle<v8::Value> v8FullClose (const v8::Arguments& args);
static v8::Handle<v8::Value> v8ForceClose (const v8::Arguments& args); static v8::Handle<v8::Value> v8ForceClose (const v8::Arguments& args);
static v8::Handle<v8::Value> EncodingGetter (v8::Local<v8::String> _, const v8::AccessorInfo& info);
static void EncodingSetter (v8::Local<v8::String> _, v8::Local<v8::Value> value, const v8::AccessorInfo& info);
Connection (v8::Handle<v8::Object> handle, v8::Handle<v8::Function> protocol_class); Connection (v8::Handle<v8::Object> handle);
virtual ~Connection (); virtual ~Connection ();
int Connect (struct addrinfo *address) { return oi_socket_connect (&socket_, address); } int Connect (struct addrinfo *address) { return oi_socket_connect (&socket_, address); }
@ -105,11 +107,11 @@ protected:
static v8::Handle<v8::Value> v8Close (const v8::Arguments& args); static v8::Handle<v8::Value> v8Close (const v8::Arguments& args);
Acceptor (v8::Handle<v8::Object> handle, Acceptor (v8::Handle<v8::Object> handle,
v8::Handle<v8::Function> protocol_class, v8::Handle<v8::Function> connection_handler,
v8::Handle<v8::Object> options); v8::Handle<v8::Object> options);
virtual ~Acceptor () { Close(); puts("acceptor gc'd!");} virtual ~Acceptor () { Close(); puts("acceptor gc'd!");}
v8::Local<v8::Function> GetProtocolClass (void); v8::Local<v8::Function> GetConnectionHandler (void);
int Listen (struct addrinfo *address) { int Listen (struct addrinfo *address) {
int r = oi_server_listen (&server_, address); int r = oi_server_listen (&server_, address);

44
test/test-pingpong.js

@ -5,14 +5,13 @@ var N = 1000;
var count = 0; var count = 0;
function Ponger (socket) { function Ponger (socket) {
this.encoding = "UTF8"; socket.encoding = "UTF8";
this.timeout = 0; socket.timeout = 0;
this.onConnect = function () { puts("got socket.");
puts("got socket.");
};
this.onReceive = function (data) { socket.onReceive = function (data) {
//puts("server recved data: " + JSON.stringify(data));
assertTrue(count <= N); assertTrue(count <= N);
stdout.print("-"); stdout.print("-");
if (/PING/.exec(data)) { if (/PING/.exec(data)) {
@ -20,46 +19,47 @@ function Ponger (socket) {
} }
}; };
this.onEOF = function () { socket.onEOF = function () {
puts("ponger: onEOF"); puts("ponger: onEOF");
socket.close(); socket.close();
}; };
this.onDisconnect = function () { socket.onDisconnect = function () {
puts("ponger: onDisconnect"); puts("ponger: onDisconnect");
socket.server.close(); socket.server.close();
}; };
} }
function Pinger (socket) { function onLoad() {
this.encoding = "UTF8"; var server = new node.tcp.Server(Ponger);
server.listen(port);
var client = new node.tcp.Connection();
client.encoding = "UTF8";
this.onConnect = function () { client.onConnect = function () {
socket.send("PING"); puts("client is connected.");
client.send("PING");
}; };
this.onReceive = function (data) { client.onReceive = function (data) {
//puts("client recved data: " + JSON.stringify(data));
stdout.print("."); stdout.print(".");
assertEquals("PONG", data); assertEquals("PONG", data);
count += 1; count += 1;
if (count < N) { if (count < N) {
socket.send("PING"); client.send("PING");
} else { } else {
puts("sending FIN"); puts("sending FIN");
socket.close(); client.close();
} }
}; };
this.onEOF = function () { client.onEOF = function () {
puts("pinger: onEOF"); puts("pinger: onEOF");
assertEquals(N, count); assertEquals(N, count);
}; };
}
function onLoad() {
var server = new node.tcp.Server(Ponger);
server.listen(port);
var client = new node.tcp.Connection(Pinger);
client.connect(port); client.connect(port);
} }

Loading…
Cancel
Save