Working with TCP sockets and servers

To send data over a network, the data has to conform to a certain format or protocol. The Transmission Control Protocol / Internet Protocol (TCP/IP) is one of the core protocols to be used on the internet.

The following screenshot shows how to communicate over TCP/IP between a Julia TCP server and a client (see the code in Chapter 8 cpserver.jl):

The server (in the upper-left corner) is started in a Julia session with server = Sockets.listen(8080), which returns a TcpServer object listening on port 8080. The conn = accept(server) line waits for an incoming client to make a connection. In a second terminal (in the lower-right corner), we start the netcat (nc) tool at the prompt to make a connection with the Julia server on port 8080, for example, nc localhost 8080. Then, the accept function creates a TcpSocket object on which the server can read or write.

Then, the server issues the line = readline(conn) command, blocking the server until it gets a full line (ending with a newline character) from the client. The client types "hello Julia server!" followed by Enter, which appears at the server console. The server can also write text to the client over the TCP connection with the write(conn, "message ") function, which then appears at the client side. The server can, when finished, close the TcpSocket connection to close the TCP connection with close(conn); this also closes the netcat session.

Of course, a normal server must be able to handle multiple clients. Here, you can see the code for a server that echoes back to the clients everything they send to the server:

// code in Chapter8echoserver.jl 
using Sockets 
server = Sockets.listen(8080) 
while true 
  conn = accept(server)  @async begin 
    try 
      while true 
        line = readline(conn)
println(line) # output in server console
write(conn,line)
end catch ex print("connection ended with error $ex") end
end # end coroutine block end

To achieve this, we place the accept() function within an infinite while loop, so that each incoming connection is accepted. The same is true for reading and writing to a specific client; the server only stops listening to that client when the client disconnects. Because the network communication with the clients is a possible source of errors, we have to surround it within a try/catch expression. When an error occurs, it is bound to the ex object. For example, when a client terminal exits, you get the connection ended with error ErrorException("stream is closed or unusable") message.

However, we also see an @async macro here; what is its function? The @async macro starts a new coroutine (refer to the Tasks section in Chapter 4, Control Flow) in the local process to handle the execution of the begin...end block that starts right after it. So, the @async macro handles the connection with each particular client in a separate coroutine. Thus, the @async block returns immediately, enabling the server to continue accepting new connections through the outer while loop. Because coroutines have a very low overhead, making a new one for each connection is perfectly acceptable. If it weren't for the async block, the program would block it until it was done with its current client before accepting a new connection.

On the other hand, the @sync macro is used to enclose a number of @async (or @spawn or @parallel calls, refer to the Parallel operations and computing section), and the code execution waits at the end of the @sync block until all the enclosed calls are finished.

Start this server example by typing the following command:

julia echoserver.jl

We can experiment with a number of netcat sessions in separate terminals. Client sessions can also be made by typing in a Julia console:

  using Sockets
conn = Sockets.connect(8080)
#> TCPSocket(Base.Libc.WindowsRawSocket(0x0000000000000340) open, 0 bytes waiting)
write(conn, "Do you hear me? ")

The listen function has some variants, for example, listen(IPv6(0),2001) creates a TCP server that listens on port 2001 on all IPv6 interfaces. Similarly, instead of readline, there are also simpler read methods:

  • read(conn, UInt8): This method blocks until there is a byte to read from conn, and then returns it. Use convert(Char, n) to convert a UInt8 value into Char. This will let you see the ASCII letter for UInt8 you read in.
  • read(conn, Char): This method blocks until there is a byte to read from conn, and then returns it.

The important aspect about the communication API is that the code looks like synchronous code executing line by line, even though the I/O is actually happening asynchronously through the use of tasks. We don't have to worry about writing callbacks as in some other languages. For more details about possible methods, refer to the I/O and Network section at https://docs.julialang.org/en/latest/base/io-network/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.159.223