Last Updated:

Client-server programming

Client-server programming java

Java is a language oriented primarily for creating a network infrastructure. And he's really strong at that. Most of the well-known services and portals are partially written in Java - Odnoklassniki, M ail.ru, Vkontakte, Google and many others. Java is good because it copes well with a high load, provided that the server is written correctly and is able to process a lot of requests per unit of time.

Sockets and their features

In order to access each other on a network, nodes must be identified somehow. And the identifier should be unique. To do this, IP addresses are used. Physically, it is simply a series of numbers from 0 to 255 separated by dots. Each node in the network, whether it is a website or a router, has its own unique IP.

Sometimes the ISP distributes one common, gray address to access the external network. But how do network services and programs communicate with each other, knowing only IP? The fact is that, knowing only the IP, the program will not be able to send data to another node. Because in addition to the destination program, many other products can be installed on the computer that are waiting for data from the network. Therefore, a port is used to identify the network program on the host.

A port is simply a number between 0 and 65535. Part of the range is reserved for the needs of protocols. And to access a certain program on a certain node, you need to have a complete set of data - IP and port. And this set, in fact, is the socket with which programs can exchange data.

Two main classes are used to program client-server network applications:

  • Socket,
  • ServerSocket.

Socket

Socket(String hostname, int port) throws Unknown Host exception, IOException
Socket(InetAddressipAddress, int port) throws Unknown Host exception

As you might guess, this is a client socket. When created, the socket notifies that it wants to connect to a specific server. Its most commonly used methods are:

  • InetAddressgetInetAddress(). Returns an object of type InetAddress, which contains the hostname, ip address, and many other data;
  • intgetPort(). Returns the port on which the connection occurs;
  • intgetLocalport(). An interesting method that will return the port that is bound to this socket;
  • booleanisConnected(). Check for an existing connection. Will return true if it is set;
  • void connect(SocketAddress address). Creates a new connection at the specified address.

The socket has an AutoCloseable interface so that you can implement auto-closes if necessary.

ServerSocket

 

The server socket runs in the application backend. It has constructors:

  • ServerSocket() throws IOException
  • ServerSocket(int port) throws IOException
  • ServerSocket(int port, int maxConnections) throws IOException
  • ServerSocket(int port, int maxConnections, InetAddresslocalAddress) throws IOException

maxConnections here is the maximum queue size of the clients standing in the queue. The default size is 50.

Creating a Simple Client Server

 

Now that everything is more or less clear with the theory, you can try to implement a single-threaded simple client-server. For ease of understanding, you can implement everything directly in the main method, which should handle the exception. Therefore, we will add throwsInterruptedException to it.

First, you need to wrap all the code related to network connections in a try block. You can start the server directly in its parameters. The constructor is given a port number of 2222.

try(ServerSocketmyServer = newServerSocket(2222)) {

Now the server needs to be prepared for the fact that clients will connect to it. You can do this by using a simple Socket and the ServerSocket.accept method. Accept informs the program that a new socket can now connect to the server.

SocketmyClient = myServer.accept();

You can report this to the console:

System.out.println("Connection accepted");

Now you need to somehow process and transfer data through sockets. You can use DataInputStream and DataOutputStream to do this.

DataOutputStream out = new DataOutputStream(myClient.getOutputStream());
DataInputStream in = new DataInputStream(myClient.getInputStream());

Two streams, input and output, were created. You can report this to the console:

System.out.println("I/O stream created");

Now you need to start communicating with the connected client. To do this, use a while loop until the connection to the client is broken.

while (!myClient.isClosed()) {
System.out.println("Reading data");
String message = in.readUTF();
System.out.println("Message received from client: "+message);

Here, you need to implement a mechanism for exiting the messaging. For example, if the client sends a "q", the server will understand that it is necessary to terminate the connection. As an expression, the if block can be checked by the message for matching the character.

In the case of truth, to cause a break of our cycle. Before that, you need to send a message to the client and wait a little with sleep. Naturally, no one uses this approach on a real server. But it fits perfectly for demonstration.

If(message.equalsIgnoreCase("q")) {
System.out.println("Client disconnected from server");
out.writeUTF("Server response" + message + "-OK");
out. flush;
Thread.sleep(5000);
break;
}

In normal mode, the server works as expected - it forwards the received messages.

out.writeUTF("Server response - " + message + " - OK");
System.out.println("Server sent data to client");
out.flush();
}

When you exit the loop, that is, when the client is disconnected, you need to report it and gradually disable the flows, and then the client itself.

System.out.println ("Client Disabled");
System.out.println ("Closing connections and streams");

in.close();
out.close();
myClient.close();
System.out.println"Closing connections and threads completed");

In a real server, each exception will need to be caught and handled accordingly. Here you can just catch everything in a row and withdraw.

} catch (IOExceptione) {
e.printStackTrace();

}
}

The server is ready and waiting for a signal from a client that does not yet exist. We need to cook it too. To begin with, similar to the server, we wrap everything in a try block. As a parameter, we pass a simple socket. And in the socket constructor we pass the address of the server - localhost and the port on which it waits for the message - 2222. Next, just as in the server, you need to prepare the input and output streams. As soon as you start and create it, you can display successful connection information in the console.

try(Socket mySocket = new Socket("localhost", 2222);
BufferedReader reader =new BufferedReader(new InputStreamReader(System.in));
DataOutputStream out = new DataOutputStream(mySocket.getOutputStream());
DataInputStream in = new DataInputStream(mySocket.getInputStream()); )
{
System.out.println("Client connected to socket");
System.out.println();
System.out.println("Input and output streams initialized");

Then a loop is started, in which all the logic will spin. The client will work as long as it exists. The reader will then wait for the data to be received. And as soon as they appear, counts them in a string and gives them out to the output stream.

while(!mySocket.isOutputShutdown()){
if(reader.ready()){
System.out.println("Client is sending data...");
Thread.sleep(1000);
String clientMessage = reader.readLine();

out.writeUTF(clientMessage);
out.flush();
System.out.println("Message sent" + clientMessage + "server");
Thread.sleep(1000);

Here again we implement the exit mechanism by the symbol "q". Before you close the connection, you need to read what the server sent using in.read. If there is something, output the data to the console. In any case, after all the operations, call break for the loop.

if(clientMessage.equalsIgnoreCase(«q»)){

System.out.println ("The Client has closed the connection");
Thread.sleep(2000);

if(in.read() > -1) {
System.out.println(«Чтение…»);
String innerIn = in.readUTF();
System.out.println(innerIn);
}
break;

}

In normal mode, the client waits for the server to respond after sending the data. If received, reads and outputs to the console.

System.out.println("The client has sent a message and is waiting for data from the server");
Thread.sleep(2000);

if(in.read() > -1) {
System.out.println("Reading...");
String innerIn = in.readUTF();
System.out.println(innerIn);
}
}
}
System.out.println("Closing streams");

} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException and) {
e.printStackTrace();
}

Naturally, this is the simplest client-server and it does not take into account some features and exceptions. Nevertheless, it works and gives a basic understanding of the work and relationship of the server with the client, at least in single-user mode.

Client server in Java NIO

 

All of the previous client-server code was implemented using a legacy approach using the java.io package. Obsolete does not mean unused, since there is still a lot of legacy code. In addition, it is simply necessary to know how the mechanism of the server and client parts works under the hood.

More modern implementations use frameworks or the java.nio package. The latter offers a more optimized and cost-effective mechanism for working with data in terms of resources. The simplest client-server on NIO is quite simple and concise.

First, you create a selector. It's a channel management tool. It allows you to block, register and transfer control to new or already registered channels. You also need to create a socket channel - a special channel that works as a socket. Use the InetSocketAddress class as the exact network address and pass it to the localhost constructor and port 2222.

The bind method binds this address to the socket channel. Next, use the configureBlocking method to specify the blocking capability. Our server will run indefinitely, so let's create an infinite loop. In it, we first send a message to the console that the server is ready to accept new connections and messages. To do this, use one of the simplest log methods, which is created separately.

publicstaticvoidmain(String[] args) throwsIOException {
Selector selector = Selector.open();
ServerSocketChannelmyServerSocket = ServerSocketChannel.open();
InetSocketAddressmyAddr = new InetSocketAddress("localhost", 2222);
myServerSocket.bind(myAddr);
myServerSocket.configureBlocking(false);
int ops = myServerSocket.validOps();
SelectionKeyselectionKey = myServerSocket.register(selector, ops, null);
while (true) {
log("The server is waiting for new connections...");

Now you need to select the keys of those channels that are ready to send data and connect. Sort them out with an iterator and sort them out while there are keys. Using if, select the channel keys ready to be joined and create a socket for it. If the channel already has readable data, then just read it.

selector.select();
Set<SelectionKey>myKeys = selector.selectedKeys();
Iterator<SelectionKey>myIterator = myKeys.iterator();
while (myIterator.hasNext()) {
SelectionKeymyKey = myIterator.next();
if (myKey.isAcceptable()) {
SocketChannelmySocket = myServerSocket.accept();

mySocket.configureBlocking(false);
mySocket.register(selector, SelectionKey.OP_READ);
log(«Соединение разрешено: » + mySocket.getLocalAddress() + «\n»);
} else if (myKey.isReadable()) {
SocketChannelmySocket = (SocketChannel) myKey.channel();
ByteBuffermyBuffer = ByteBuffer.allocate(256);
mySocket.read(myBuffer);
String result = new String(myBuffer.array()).trim();
log(«Сообщение получено: » + result);

Next, we implement a client closure mechanism. The marker would be "Five." And clean up the iterator.

if (result.equals("Five")) {
mySocket.close();
log(\nAfter you receive a value of Five, you can close the connection to the client");
log(\nBut the server will continue its work");
}
}
myIterator.remove();

}
}

This completes the server code. Now you need to create the client. Immediately add to the main method the ability to throw exceptions. And by analogy with the server, we create objects with addresses and socket channel. In the body of the method, create a simple list array with the names of the numbers.

publicstaticvoidmain(String[] args) throwsIOException, InterruptedException {
InetSocketAddressmyAddr = new InetSocketAddress("localhost", 2222);
SocketChannelmySocket = SocketChannel.open(myAddr);
log("Connecting to server via port 2222...");
ArrayList<String>nums = new ArrayList<String>();
nums.add("One");
nums.add("Two");
nums.add("Three");
nums.add("Four");
nums.add("Five");

Then we go through the foreach loop and transcode each element into bytes, then into a buffer with bytes and pass it using the write method of our channel. In each iteration, clear the buffer and set the delay of a couple of seconds using the sleep method of the Thread class. After exiting the loop, close the channel.

for (Stringnum :nums) {

byte[] message = new String(num).getBytes();
ByteBuffer buffer = ByteBuffer.wrap(message);
mySocket.write(buffer);

log(«отправка» + num);
buffer.clear();
Thread.sleep(2000);
}
mySocket.close();

}

As you can see, this code is a bit simpler. Operations are performed asynchronously without blocking each other. The small log method that serves to output looks like this:

private static void log(String str) {
System.out.println(str);

}

A little bit about the features of NIO

 

Channels and selectors were introduced in NIO. A channel is a logical object that is an abstract representation of a structure— a socket or a file. A selector works with the channels. He knows how to register them, determine which channel is currently blocked, and which is working and ready to transmit something.

NIO is based on a buffer-based approach. That is, instead of streams, data is transmitted using buffers. And for each primitive there is its own wrapper. The old IO could work with threads by reading data in turn. In NIO, on the other hand, you can navigate through the buffer by retrieving the desired dataset.

Locks in IO are a different story. Input and output streams are always blocked while reading or writing. And this happens until the operation is completed. In NIO, channels are read on the readiness to give information, or are not read at all. At the moment when the stream is idle, it can turn to another channel or do something else. In this way, you can achieve asynchrony, saving resources and time.

Conclusion

 

Both kinds of client-server perfectly demonstrate how the code for creating network messaging tools actually works. And sometimes such solutions are suitable for small tasks. But it is unlikely that they will be relevant when writing a server with a load of 100,000 concurrent users. And here various frameworks are already used - half-finished solutions that simplify interaction with network protocols, sockets, channels, file transfer and other things.