As you may know, each computer on the Internet has an address to uniquely identify it and allow other computers to interact with it.
Suppose you need to write a network application such as an instant messenger or an online game. For this purpose, you need to organize interaction between multiple users of your application. You can implement this interaction using sockets.
What is a socket?
A socket is an interface to send and receive data between different processes (running programs) in bidirectional order. It is determined by the combination of the computer address in the network (e.g., 127.0.0.1, which is your own machine) and a port on this machine. A port is an integer number from 0 to 65535, preferably greater than 1024 (e.g., 8080, 32254, and so on). So, a socket may have the following address: 127.0.0.1:32245.
To start the communication, one program called a client creates a socket to request a connection with another program called a server. To this end, the client uses the server machine address and a specific port on which the server listens for incoming requests. The server waits for new requests and either accepts them or not. If a request is accepted, the server creates a socket for interaction with the client. Both the client and server can send data to each other using their sockets. As a rule, one server interacts with multiple clients. How long the interaction continues depends on the application.
Note: both the client and server programs can be either on the same computer or on different machines connected through a network.
Using sockets in Kotlin
Sockets in Kotlin are utilized with the use of two main Java classes, both located in the java.net package:
-
The
Socketclass represents one side of a two-way connection (used by clients and servers). -
The
ServerSocketclass represents a special type of socket that listens for and accepts connections to clients (only used by servers).
The following picture demonstrates when we should use both classes.
Socket and ServerSocket
So, a client program uses a Socket to send a connection request. The server has a ServerSocket to accept the request and then creates a new Socket for interaction with the client.
As an example, we will write a small echo server, which gets messages from clients and then sends them back. As a result, we will have two programs: a client and a server. Each program has its own main function.
The server-side code
First, let's consider the server-side code. To create a server socket, we use the following statement:
val server: ServerSocket = ServerSocket(34522)
The server object listens on port 34522 for connection requests from clients.
The server can accept a new client and create a socket to interact with it:
val socket: Socket = server.accept() // a socket to interact with a new client
The accept method forces the program to wait for a new client, i.e., it executes until a new client comes. After this statement, we have a socket object that can be used to interact with the client.
To send and receive data, we need input and output streams. To read data from a source, an input stream is utilized. In addition, data is written to a destination via an output stream. For example, the standard input stream gets data from the keyboard, while the standard output stream prints data to a display. In order to communicate data through sockets, the DataInputStream and DataOutputStream classes should be used. These are located in the java.io package.
val input = DataInputStream(socket.getInputStream())
val output = DataOutputStream(socket.getOutputStream())
-
The invocation
input.readUTF()receives a string message from the client. -
The invocation
output.writeUTF(message)sends a string message to the client.
If you need to send or receive something like movies or audio files, you may work directly with bytes rather than strings.
Below, you can see a full server program that accepts clients in a loop and resends messages to them.
import java.io.*
import java.net.*
const val PORT = 34522
fun main(args: Array<String>) {
try {
ServerSocket(PORT).use { server ->
while (true) {
server.accept().use { socket ->
DataInputStream(socket.getInputStream()).use { input ->
DataOutputStream(socket.getOutputStream()).use { output ->
val msg = input.readUTF() // read a message from the client
output.writeUTF(msg) // resend it to the client
}
}
}
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
As you may see, this code clearly demonstrates the basic ideas of socket communication. The program creates only a single ServerSocket and then accepts client connections in an infinite loop. The program stops when a shutdown occurs.
Pay attention: we handle exceptions in a simplified way. We also employ the use extension method so the socket and streams can be closed automatically to avoid resource leaks.
The client-side code
To start interacting with a server, we need at least one client.
First, we should create a socket, specifying the path and port of a server.
val socket = Socket("127.0.0.1", 23456) // address and port of a server
Here, we use the localhost address ("127.0.0.1") as an example. In a real case, your server will be hosted on another computer. Therefore, it is a good practice to take the address and port from an external configuration or command-line arguments.
To send and receive data to the server, we need input and output streams:
val input = DataInputStream(socket.getInputStream())
val output = DataOutputStream(socket.getOutputStream())
We have considered before how to use them.
Below is a full client program that connects to a server, sends one message, and prints the received message from the server.
import java.io.*
import java.net.*
const val SERVER_ADDRESS = "127.0.0.1"
const val SERVER_PORT = 34522
fun main(args: Array<String>) {
try {
Socket(SERVER_ADDRESS, SERVER_PORT).use { socket ->
DataInputStream(socket.getInputStream()).use { input ->
DataOutputStream(socket.getOutputStream()).use { output ->
val msg = readln()
output.writeUTF(msg) // send a message to the server
val receivedMsg = input.readUTF() // read the reply from the server
println("Received from the server: $receivedMsg")
}
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
Like before, we employ the use extension method to close the socket and streams.
To demonstrate our client program, we first need to start the server and then run one or more client programs.
> Hello!
Received from the server: Hello!
Another example:
> What?
Received from the server: What?
The symbol > is not part of the input. It just marks the input line.
Serving multiple long-connected clients
If we want to develop a chat or a game server, our clients will not stop after sending a single message. They will periodically send and receive messages to/from the server.
Let's try to improve our client to send five messages to the echo server. Here is a modified client code that should be placed inside the try statement:
for (i in 0..4) {
val msg = readln()
output.writeUTF(msg)
val receivedMsg = input.readUTF()
println(receivedMsg)
}
The server was also modified to accept all five messages from a client.
for (i in 0..4) {
val msg = input.readUTF() // read the next client message
output.writeUTF(msg) // resend it to the client
}
It will work perfectly for interacting with a single client:
> Hello1
Received from the server: Hello1
> Hello2
Received from the server: Hello2
> Hello3
Received from the server: Hello3
> Hello4
Received from the server: Hello4
> Hello5
Received from the server: Hello5
But if we start two or more clients, we will notice a strange effect. The server will not interact with the second client before responding to all messages from the first client. How come? Because we use only a single thread to process messages from all clients.
Multithreaded server
The simplest way to work with multiple clients simultaneously is to use multithreading! Let one server thread (e.g., main) accept new clients while others interact with the already accepted clients (one thread per client).
Here is our modified server with a new abstraction called Session. Since each session works in a separate thread, we can run multiple sessions simultaneously.
import java.io.*
import java.net.*
const val PORT = 34522
fun main(args: Array<String>) {
try {
ServerSocket(PORT).use { server ->
while (true) {
val session = Session(server.accept())
session.start() // does not block this server thread
println(session)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
class Session(private val socket: Socket) : Thread() {
override fun run() {
try {
DataInputStream(socket.getInputStream()).use { input ->
DataOutputStream(socket.getOutputStream()).use { output ->
for (i in 0..4) {
val msg = input.readUTF()
output.writeUTF(msg)
}
socket.close()
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
If we start this version of the server and several clients, all clients will be able to receive messages.
It is important to note that we do not employ the use extension method for server.accept() because we create a socket in one thread and close it in another. In this case, it would be very error-prone, since one thread could close the socket while another wants to use it.
Conclusion
We hope you enjoy developing your own server in Kotlin! You can now use this knowledge to create a chat or file storage system.