Java version note: The classic socket APIs used in this unit are still supported in current Java releases. The main examples use long-established APIs; the optional virtual-thread example in section 3 requires Java 21 or later.
A socket is an endpoint that allows two processes to exchange information through a network. The processes may run on the same computer or on different computers. In practice, a socket is associated with a network address and a port number, and the operating system uses this information to deliver data to the appropriate process.
A port number identifies a communication endpoint inside a machine. The combination of an IP address and a port number identifies one endpoint of a network communication.
For example, a server application may listen on TCP port 6000. When a packet addressed to that machine and port reaches the operating system, it can be delivered to the application that owns that socket.
Port numbers range from 0 to 65535. Ports from 0 to 1023 are known as well-known ports and are traditionally assigned to standard services. For our own classroom applications, we will normally choose a higher port that is not already being used by another application.
For instance, if a machine A has a process PA listening on port 6000, a remote process can contact it by using machine A’s IP address (or host name) and port 6000.
Client-server communication is a common type of network communication where:
When a TCP client connects to a server, both sides establish a communication channel. The client can send data to the server, and the server can send data back to that particular client.
For instance, web servers commonly use port 80 for HTTP and port 443 for HTTPS. A browser normally connects to the server using its domain name, which is resolved to an IP address, and the corresponding service port.
The server normally listens on a known port, whereas the client’s local port is usually selected automatically by the operating system from an ephemeral port range.
For example, a client may connect from local port 52431 to a server listening on port 6000. The connection can then be identified by the two endpoints: client-address:52431 and server-address:6000.
When the server calls accept(), Java creates a new Socket object dedicated to that client. However, the accepted socket still uses the same local server port (for example, 6000). The listening ServerSocket remains available to accept more clients. Different TCP connections are distinguished by their combination of local and remote addresses and ports.
The two transport protocols that we will mainly use are TCP and UDP:
Therefore, we normally choose TCP when reliable ordered delivery is required, and UDP when independent datagrams and low overhead are more appropriate for the application.
Distributed applications can use different communication models. Some of the most common are:
| Model | Main idea | Typical example |
|---|---|---|
| Client-server | One or more clients request a service from a server that listens on a known address and port. | Web applications, database servers, multiplayer game servers |
| Peer-to-peer (P2P) | Each participant can act as both client and server, communicating directly with other peers. | File-sharing or decentralized applications |
| Unicast | One sender communicates with one receiver. | A normal TCP connection between one client and one server |
| Broadcast | One sender sends a datagram to all hosts in a broadcast domain. | Local network discovery protocols |
| Multicast | One sender sends data to a group of interested receivers. | Group notifications or multimedia distribution |
We can also distinguish between request-response communication, where one side sends a request and waits for a response, and more asynchronous communication, where messages can arrive independently of a previous request. The appropriate model depends on the number of participants, reliability requirements, latency and the way the application is designed.
In this section we are going to learn how to deal with TCP and UDP sockets in Java. The main networking classes are defined in the java.net package. We will use the classic blocking socket API because it is simple and appropriate for learning the basic concepts of network programming.
You can also download here the source code of the examples that we are going to explain in this section, so you can test them easily.
If we want to work with TCP sockets, we will mainly use these two classes:
ServerSocket, used on the server side to listen for client connection requests.Socket, which represents one established TCP connection. We use a Socket object on both the client and the server side.In order to connect a client to a server, we will follow these steps.
On the server side:
ServerSocket specifying the desired port number.accept(). This method blocks until a connection is available, unless a timeout has been configured.accept() returns a Socket object dedicated to that client.ServerSocket when the server is no longer going to accept new connections.For example:
try (
ServerSocket server = new ServerSocket(portNumber);
Socket service = server.accept();
DataInputStream socketIn =
new DataInputStream(service.getInputStream());
DataOutputStream socketOut =
new DataOutputStream(service.getOutputStream())
)
{
// Communication process
} catch (IOException e) {
System.out.println(e.getMessage());
}
On the client side:
Socket with the server address and port.For example:
try (
Socket mySocket = new Socket("server-address", portNumber);
DataInputStream socketIn =
new DataInputStream(mySocket.getInputStream());
DataOutputStream socketOut =
new DataOutputStream(mySocket.getOutputStream())
)
{
// Communication process
} catch (IOException e) {
System.out.println(e.getMessage());
}
Let’s implement our first complete example. We are going to create a client that sends "Hello" to the server, and a server that receives this message and sends "Goodbye" back to the client.
A real project would normally keep the client and server in separate modules or projects, because each side can be distributed and executed independently.
Client:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class Greet_Client
{
public static void main(String[] args)
{
try (
Socket mySocket = new Socket("localhost", 2000);
DataInputStream socketIn =
new DataInputStream(mySocket.getInputStream());
DataOutputStream socketOut =
new DataOutputStream(mySocket.getOutputStream())
)
{
socketOut.writeUTF("Hello");
String response = socketIn.readUTF();
System.out.println("Received: " + response);
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
Server:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Greet_Server
{
public static void main(String[] args)
{
try (
ServerSocket server = new ServerSocket(2000);
Socket service = server.accept();
DataInputStream socketIn =
new DataInputStream(service.getInputStream());
DataOutputStream socketOut =
new DataOutputStream(service.getOutputStream())
)
{
String message = socketIn.readUTF();
System.out.println("Received: " + message);
socketOut.writeUTF("Goodbye");
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
To run this example, start the server first. When it is waiting for connections, run the client. The server console should show Received: Hello, and the client console should show Received: Goodbye.
The address localhost refers to the local computer. It is useful for testing both applications on the same machine. To test them on two different machines, the client must use a host name or IP address that reaches the server, and the operating system/firewall must allow the selected port.
In the previous examples we used DataInputStream and DataOutputStream. They are convenient when both sides are Java applications and we want to send Java primitive data types or short strings in a defined binary format.
We can also use other stream classes. For example, BufferedReader together with PrintWriter or BufferedWriter is useful for a text protocol that sends one message per line.
It is important to remember that TCP is a byte stream, not a message protocol. If an application sends several values, both sides must agree on their order and format. For example, if one side sends data with writeUTF(), the other side should read it with readUTF(). Mixing incompatible reading and writing methods may make the receiver block or interpret the data incorrectly.
try-with-resources can and should be used whenever the lifetime of the resources matches the corresponding block. In a simple one-client server, all resources can be opened and closed in the same block. In a multi-client server, the listening ServerSocket belongs to the main server loop, while each accepted Socket belongs to the code that handles that particular client.
For long-lived graphical or event-driven applications, the socket may need to remain open across several event handlers. In that case, the application must keep the socket as part of its state and close it explicitly when the connection or application ends.
Regarding the classes that we have just learnt, these constructors and methods are useful:
| Method(s) | Description |
|---|---|
ServerSocket(int port) |
Creates a server socket bound to the specified local port. Port 0 can be used when we want the system to select an available port automatically. |
ServerSocket(int port, int backlog) |
Creates a server socket with a requested maximum queue length for pending incoming connections. backlog is not the maximum number of clients that the application can serve simultaneously. |
Socket(String host, int port) / Socket(InetAddress addr, int port) |
Creates a TCP socket and connects it to the specified remote host and port. |
int getLocalPort(), int getPort() |
For a connected Socket, they return the local port and the remote port, respectively. |
setSoTimeout(int milliseconds) |
Sets a maximum blocking time for some read operations. If the timeout expires, a SocketTimeoutException is thrown. |
readByte(), readInt(), readDouble(), readUTF() |
Methods of DataInputStream for reading values written in the corresponding binary format. readUTF() reads the modified UTF-8 format used by writeUTF(). |
writeByte(...), writeInt(...), writeDouble(...), writeUTF(String) |
Methods of DataOutputStream for writing values in a portable binary format. writeUTF() and readUTF() should be used as a matching pair. |
println(String) |
A method commonly used with PrintWriter to send a line of text. |
readLine() |
A method of BufferedReader that reads a line of text. It is useful when the sender also uses a line-oriented text protocol. |
Exercise 1:
Create an “echo” client-server application using Java sockets, by creating two separate projects (one for the client and another one for the server):
- Create a project for the server side called Echo_Server. Define a server that listens on port 6000. When it gets a connection, it must repeatedly read a message from the client and send the same message back converted to uppercase. For instance, if it receives
"Hello", it will return"HELLO".- Create a project for the client side called Echo_Client. Define a socket that connects to the server. Use
localhostas the server name if you are running both projects on the same machine. Once connected, the client repeatedly asks the user to enter a message, sends it to the server, and waits for the corresponding echo.- The communication process will finish when the server receives the message
"bye".
UDP does not establish a reliable byte-stream connection like TCP. Applications send and receive independent datagrams. Each outgoing datagram identifies its destination address and port, and each received datagram contains information about the sender.
We will mainly work with the DatagramSocket and DatagramPacket classes.
The basic steps are:
DatagramSocket and bind it to a local port when necessary:
DatagramSocket socket = new DatagramSocket();DatagramSocket socket = new DatagramSocket(portNumber);DatagramSocket socket = new DatagramSocket(portNumber, localAddress);Notice that the address in the third constructor is a local bind address, not the remote destination. The remote destination is normally specified in the outgoing DatagramPacket.
DatagramPacket with the data and destination, and send it by calling send() on the DatagramSocket. To receive data, create a packet backed by a buffer and call receive() on the socket.String text = "Hello";
byte[] message = text.getBytes(StandardCharsets.UTF_8);
DatagramPacket packetS = new DatagramPacket(
message,
message.length,
InetAddress.getLoopbackAddress(),
2000
);
socket.send(packetS);
byte[] buffer = new byte[1024];
DatagramPacket packetR = new DatagramPacket(buffer, buffer.length);
socket.receive(packetR);
String receivedText = new String(
packetR.getData(),
packetR.getOffset(),
packetR.getLength(),
StandardCharsets.UTF_8
);
Using packetR.getLength() is important because the receive buffer may be larger than the datagram that actually arrived.
setSoTimeout(). If no datagram is received before the timeout expires, Java throws a SocketTimeoutException.socket.setSoTimeout(2000); // 2 seconds
try {
socket.receive(packetR);
} catch (SocketTimeoutException e) {
System.out.println("Timeout while waiting for a response");
}
DatagramSocket implements AutoCloseable, it can be used with try-with-resources.We are going to implement the same greeting example using UDP.
Client:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
public class GreetUDP_Client
{
public static void main(String[] args)
{
try (DatagramSocket mySocket = new DatagramSocket())
{
String text = "Hello";
byte[] message = text.getBytes(StandardCharsets.UTF_8);
DatagramPacket packetS = new DatagramPacket(
message,
message.length,
InetAddress.getLoopbackAddress(),
2000
);
mySocket.send(packetS);
byte[] buffer = new byte[1024];
DatagramPacket packetR = new DatagramPacket(buffer, buffer.length);
mySocket.receive(packetR);
String response = new String(
packetR.getData(),
packetR.getOffset(),
packetR.getLength(),
StandardCharsets.UTF_8
);
System.out.println("Received: " + response);
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
Server:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
public class GreetUDP_Server
{
public static void main(String[] args)
{
try (DatagramSocket mySocket = new DatagramSocket(2000))
{
byte[] buffer = new byte[1024];
DatagramPacket packetR = new DatagramPacket(buffer, buffer.length);
mySocket.receive(packetR);
String received = new String(
packetR.getData(),
packetR.getOffset(),
packetR.getLength(),
StandardCharsets.UTF_8
);
System.out.println("Received: " + received);
int destPort = packetR.getPort();
InetAddress destAddr = packetR.getAddress();
String text = "Goodbye";
byte[] message = text.getBytes(StandardCharsets.UTF_8);
DatagramPacket packetS = new DatagramPacket(
message,
message.length,
destAddr,
destPort
);
mySocket.send(packetS);
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
Binding the server with new DatagramSocket(2000) allows it to receive datagrams sent to port 2000 on the local interfaces accepted by the operating system. Binding explicitly to a particular local address is only necessary when we want to restrict the socket to that interface/address.
An outgoing UDP datagram includes:
When a datagram is received, Java makes the sender’s address and port available through getAddress() and getPort().
For text messages, it is a good practice to use an explicit character encoding such as StandardCharsets.UTF_8 on both sides. When converting a received datagram to a String, use the packet’s actual offset and length instead of converting the complete receive buffer.
Also remember that UDP does not guarantee that a datagram will arrive, arrive only once, or arrive in the same order as previous datagrams. If the application needs those guarantees, it must implement them itself or use a protocol such as TCP.
Finally, the receive buffer must be large enough for the expected datagram. If the incoming datagram is larger than the buffer supplied to the DatagramPacket, the received data may be truncated.
Exercise 2:
Create a UDP client-server application with the following projects:
- A project called UDPDictionary_Client that sends to the server a word typed by the user. Set a timeout in the client (for instance, 5 seconds) using
setSoTimeout(). If aSocketTimeoutExceptionoccurs, the client must print"No translation found".- A project called UDPDictionary_Server that runs on port 6000. It will have a
Map<String, String>(for example, aHashMap) containing some words in English and their Spanish translations. The server will read the word sent by the client and return its translation. If the word cannot be found, the server will not send a response, so the client’s timeout will eventually expire.
The InetAddress class represents an IP address and provides methods for resolving host names through the system-wide name resolver.
A common way to obtain an address is:
InetAddress address = InetAddress.getByName("www.example.com");
If the argument is a host name, Java asks the configured resolver for its address. The exact resolution mechanism may involve DNS, local configuration and caching, depending on the operating system and Java runtime.
We can also create an InetAddress from a textual IP address:
InetAddress address = InetAddress.getByName("203.0.113.10");
In this case, the textual IP address itself can be parsed without requiring a forward DNS lookup. Calling getHostName() later may trigger name resolution in order to obtain a host name.
Some useful methods are:
getHostAddress(): returns the textual IP address.getHostName(): returns the host name and may perform name resolution.getAllByName(String): returns all addresses resolved for a host name.getLoopbackAddress(): returns a loopback address and is useful when client and server run on the same machine.Sending data through a normal TCP socket does not provide encryption by itself. TLS (Transport Layer Security) adds confidentiality, integrity and authentication to a TCP connection. The older SSL protocols are obsolete, although Java’s API still uses historical class names such as SSLSocket and SSLServerSocket.
Current Java implementations support modern TLS versions such as TLS 1.2 and TLS 1.3.
SSLSocket and SSLServerSocketThe javax.net.ssl package includes the SSLSocket and SSLServerSocket classes. They extend the normal socket classes with TLS functionality.
On the server side:
SSLServerSocket using an SSLServerSocketFactory.SSLSocket objects.On the client side:
SSLSocket using an SSLSocketFactory.The following example is intentionally simple and intended for local classroom testing with the keystore and truststore created in the annex.
Server code:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
public class SecureServer {
public static void main(String[] args) throws Exception {
System.setProperty("javax.net.ssl.keyStore", "server.keystore");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
SSLServerSocketFactory ssf =
(SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
try (SSLServerSocket serverSocket =
(SSLServerSocket) ssf.createServerSocket(8443)) {
System.out.println("Secure server running on port 8443...");
try (SSLSocket clientSocket =
(SSLSocket) serverSocket.accept();
BufferedReader in = new BufferedReader(
new InputStreamReader(
clientSocket.getInputStream(),
StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(
clientSocket.getOutputStream(),
StandardCharsets.UTF_8),
true)) {
System.out.println("Client connected!");
String message = in.readLine();
System.out.println("Received: " + message);
out.println("Goodbye");
}
}
}
}
Client code:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
public class SecureClient {
public static void main(String[] args) throws Exception {
System.setProperty("javax.net.ssl.trustStore", "client.truststore");
System.setProperty("javax.net.ssl.trustStorePassword", "password");
SSLSocketFactory ssf =
(SSLSocketFactory) SSLSocketFactory.getDefault();
try (SSLSocket socket =
(SSLSocket) ssf.createSocket("localhost", 8443);
BufferedReader in = new BufferedReader(
new InputStreamReader(
socket.getInputStream(),
StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(
socket.getOutputStream(),
StandardCharsets.UTF_8),
true)) {
out.println("Hello");
String response = in.readLine();
System.out.println("Received: " + response);
}
}
}
SSLSocket applications should also perform endpoint identification for real deployments. Higher-level HTTPS APIs already define hostname-verification rules."password" are only acceptable in a controlled teaching example. Production credentials must not be embedded directly in source code.Exercise 3:
Implement a secure client-server application using
SSLSocketandSSLServerSocket. Create the keystore and truststore files using thekeytoolutility and test the application locally.
A server that accepts only one client at a time is not suitable for many real applications. A simple way to serve several TCP clients concurrently is to keep the ServerSocket in the main server thread and create a separate thread for each accepted client.
try (ServerSocket server = new ServerSocket(PORT))
{
System.out.println("Listening...");
while (true)
{
Socket service = server.accept();
System.out.println(
"Connection established with " +
service.getRemoteSocketAddress()
);
ServerThread st = new ServerThread(service);
st.start();
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
The thread owns the socket corresponding to its client. Therefore, it can safely close that socket when the client session finishes:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class ServerThread extends Thread
{
private final Socket service;
public ServerThread(Socket service)
{
this.service = service;
}
@Override
public void run()
{
try (
Socket client = service;
DataInputStream socketIn =
new DataInputStream(client.getInputStream());
DataOutputStream socketOut =
new DataOutputStream(client.getOutputStream())
)
{
// Communication with this client
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
The important idea is resource ownership: the main server loop owns the ServerSocket, while each worker thread owns one accepted client Socket.
The thread-per-client model is easy to understand and is appropriate for this unit. In modern Java (Java 21 and later), virtual threads are another option for applications that need to handle many blocking client connections. For example, instead of creating a traditional platform thread explicitly, a server can start a virtual thread for a client task:
Socket service = server.accept();
Thread.startVirtualThread(() -> handleClient(service));
Virtual threads do not change the socket protocol or the client-server design; they are a more scalable way of running many blocking tasks. The classic Thread solution above is still perfectly valid for learning the concepts required in this unit.
With UDP, the server does not need a separate connection object for every client because every received datagram already contains the sender address and port. A simple UDP server can therefore receive and process datagrams sequentially in a loop. Threads may still be useful if processing each datagram is expensive or must run concurrently, but they are not required merely to distinguish one UDP client from another.
while (true)
{
byte[] received = new byte[1024];
DatagramPacket datagramReceived =
new DatagramPacket(received, received.length);
mySocket.receive(datagramReceived);
InetAddress remoteIP = datagramReceived.getAddress();
int remotePort = datagramReceived.getPort();
byte[] sent = createResponse(datagramReceived);
DatagramPacket datagramSent = new DatagramPacket(
sent,
sent.length,
remoteIP,
remotePort
);
mySocket.send(datagramSent);
}
Exercise 4:
Improve Exercise 1 with these changes:
- Make the client-server connection independent from the machines where the applications are placed. Do not hard-code
localhostas the only possible server address. Let the user specify the server address.- Allow more than one client to connect to the server at the same time. The server must handle each TCP client independently and return the correct echo to each one.
- Test the program with at least two clients connected simultaneously.
- Call the new projects EchoImproved_Server and EchoImproved_Client.
Network applications can fail even when the program compiles correctly. The client and server run independently, depend on addresses and ports, and may block while waiting for data. For this reason, systematic testing and useful diagnostic information are especially important.
A useful sequence is:
localhost or the loopback address."bye".| Exception | Typical cause |
|---|---|
UnknownHostException |
The host name cannot be resolved. |
ConnectException |
The TCP connection cannot be established, for example because no server is listening on that address/port or the connection is refused. |
BindException |
The local address/port cannot be bound, often because the port is already in use. |
SocketTimeoutException |
A configured timeout expired while waiting for a connection or data. |
EOFException / end of stream |
The peer closed the connection while the application was expecting more data. |
SocketException |
A general socket-level problem, including some cases where a connection has been closed or reset. |
Do not hide every exception with an empty catch block. During development, print enough information to understand what failed. For example, log the exception message together with the client address, port and operation being performed.
For larger applications, a logging framework or java.util.logging is preferable to many unrelated System.out.println() calls, but console messages are sufficient for the introductory exercises in this unit.
When debugging a socket application, check both sides of the protocol:
writeUTF() with readUTF(), line-based output with readLine(), etc.)?Socket?Breakpoints are particularly useful immediately before and after blocking operations such as accept(), read...() and receive(). If execution reaches a read operation and never continues, inspect what the other side is expected to send next.
Every exercise or project should document at least:
String command -> int value -> String response.Classes and non-obvious methods should also include meaningful comments or JavaDoc. Documentation should explain why the communication is organised in that way, not simply repeat what each Java instruction already says.
Exercise 5 — Debugging and documentation:
Use EchoImproved_Server and EchoImproved_Client from Exercise 4.
- Test and record the result of at least these situations: normal connection, wrong server port, server not running, client closing unexpectedly, and two clients connected simultaneously.
- Improve the exception handling so that the user receives meaningful messages instead of an unexplained stack trace or a program that appears to freeze.
- Use the debugger to follow at least one complete request-response exchange and identify where the server waits for a connection and where it waits for client data.
- Add a short
README.mddocumenting the protocol, server port, how to run both projects, the"bye"termination rule, and the tests performed.- Add appropriate JavaDoc or comments to the classes and methods responsible for network communication.